@brandlab-digital/site-next 3.13.0 → 3.14.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 (169) hide show
  1. package/README.md +79 -0
  2. package/dist/contact/delivery.d.ts +81 -0
  3. package/dist/contact/delivery.d.ts.map +1 -0
  4. package/dist/contact/delivery.js +138 -0
  5. package/dist/contact/delivery.js.map +1 -0
  6. package/dist/contact/resend-webhook.d.ts +72 -0
  7. package/dist/contact/resend-webhook.d.ts.map +1 -0
  8. package/dist/contact/resend-webhook.js +300 -0
  9. package/dist/contact/resend-webhook.js.map +1 -0
  10. package/dist/contact/route-handler.d.ts +16 -0
  11. package/dist/contact/route-handler.d.ts.map +1 -1
  12. package/dist/contact/route-handler.js +8 -0
  13. package/dist/contact/route-handler.js.map +1 -1
  14. package/dist/contact/server-entry.d.ts +2 -0
  15. package/dist/contact/server-entry.d.ts.map +1 -1
  16. package/dist/contact/server-entry.js +2 -0
  17. package/dist/contact/server-entry.js.map +1 -1
  18. package/dist/contact/server.d.ts +26 -0
  19. package/dist/contact/server.d.ts.map +1 -1
  20. package/dist/contact/server.js +48 -3
  21. package/dist/contact/server.js.map +1 -1
  22. package/dist/next-config.d.ts +17 -1
  23. package/dist/next-config.d.ts.map +1 -1
  24. package/dist/next-config.js +36 -2
  25. package/dist/next-config.js.map +1 -1
  26. package/dist/ui-kit/brandlab-skin.d.ts +24 -1
  27. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  28. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  29. package/dist/ui-kit/form/photo-upload.d.ts +4 -1
  30. package/dist/ui-kit/form/photo-upload.d.ts.map +1 -1
  31. package/dist/ui-kit/form/photo-upload.js +4 -4
  32. package/dist/ui-kit/form/photo-upload.js.map +1 -1
  33. package/dist/ui-kit/form/quote-form.d.ts +4 -1
  34. package/dist/ui-kit/form/quote-form.d.ts.map +1 -1
  35. package/dist/ui-kit/form/quote-form.js.map +1 -1
  36. package/dist/ui-kit/form/thank-you.d.ts +4 -1
  37. package/dist/ui-kit/form/thank-you.d.ts.map +1 -1
  38. package/dist/ui-kit/form/thank-you.js +2 -2
  39. package/dist/ui-kit/form/thank-you.js.map +1 -1
  40. package/dist/ui-kit/icon-glyphs.d.ts +41 -0
  41. package/dist/ui-kit/icon-glyphs.d.ts.map +1 -0
  42. package/dist/ui-kit/icon-glyphs.js +138 -0
  43. package/dist/ui-kit/icon-glyphs.js.map +1 -0
  44. package/dist/ui-kit/icon-sprite.d.ts +19 -0
  45. package/dist/ui-kit/icon-sprite.d.ts.map +1 -0
  46. package/dist/ui-kit/icon-sprite.js +44 -0
  47. package/dist/ui-kit/icon-sprite.js.map +1 -0
  48. package/dist/ui-kit/icons.d.ts +38 -3
  49. package/dist/ui-kit/icons.d.ts.map +1 -1
  50. package/dist/ui-kit/icons.js +47 -64
  51. package/dist/ui-kit/icons.js.map +1 -1
  52. package/dist/ui-kit/primitives/badge.d.ts +25 -0
  53. package/dist/ui-kit/primitives/badge.d.ts.map +1 -0
  54. package/dist/ui-kit/primitives/badge.js +21 -0
  55. package/dist/ui-kit/primitives/badge.js.map +1 -0
  56. package/dist/ui-kit/primitives/breadcrumbs.d.ts +27 -0
  57. package/dist/ui-kit/primitives/breadcrumbs.d.ts.map +1 -0
  58. package/dist/ui-kit/primitives/breadcrumbs.js +40 -0
  59. package/dist/ui-kit/primitives/breadcrumbs.js.map +1 -0
  60. package/dist/ui-kit/primitives/button.d.ts +32 -0
  61. package/dist/ui-kit/primitives/button.d.ts.map +1 -0
  62. package/dist/ui-kit/primitives/button.js +8 -0
  63. package/dist/ui-kit/primitives/button.js.map +1 -0
  64. package/dist/ui-kit/primitives/card.d.ts +40 -0
  65. package/dist/ui-kit/primitives/card.d.ts.map +1 -0
  66. package/dist/ui-kit/primitives/card.js +8 -0
  67. package/dist/ui-kit/primitives/card.js.map +1 -0
  68. package/dist/ui-kit/primitives/index.d.ts +11 -2
  69. package/dist/ui-kit/primitives/index.d.ts.map +1 -1
  70. package/dist/ui-kit/primitives/index.js +11 -2
  71. package/dist/ui-kit/primitives/index.js.map +1 -1
  72. package/dist/ui-kit/primitives/layout.d.ts +12 -0
  73. package/dist/ui-kit/primitives/layout.d.ts.map +1 -1
  74. package/dist/ui-kit/primitives/layout.js +20 -2
  75. package/dist/ui-kit/primitives/layout.js.map +1 -1
  76. package/dist/ui-kit/primitives/links.d.ts +15 -0
  77. package/dist/ui-kit/primitives/links.d.ts.map +1 -1
  78. package/dist/ui-kit/primitives/links.js +5 -1
  79. package/dist/ui-kit/primitives/links.js.map +1 -1
  80. package/dist/ui-kit/primitives/lists.d.ts +43 -0
  81. package/dist/ui-kit/primitives/lists.d.ts.map +1 -0
  82. package/dist/ui-kit/primitives/lists.js +34 -0
  83. package/dist/ui-kit/primitives/lists.js.map +1 -0
  84. package/dist/ui-kit/primitives/media-frame.d.ts +28 -0
  85. package/dist/ui-kit/primitives/media-frame.d.ts.map +1 -0
  86. package/dist/ui-kit/primitives/media-frame.js +58 -0
  87. package/dist/ui-kit/primitives/media-frame.js.map +1 -0
  88. package/dist/ui-kit/primitives/media.d.ts +38 -0
  89. package/dist/ui-kit/primitives/media.d.ts.map +1 -0
  90. package/dist/ui-kit/primitives/media.js +9 -0
  91. package/dist/ui-kit/primitives/media.js.map +1 -0
  92. package/dist/ui-kit/primitives/stat.d.ts +25 -0
  93. package/dist/ui-kit/primitives/stat.d.ts.map +1 -0
  94. package/dist/ui-kit/primitives/stat.js +7 -0
  95. package/dist/ui-kit/primitives/stat.js.map +1 -0
  96. package/dist/ui-kit/repeated-slots.d.ts +121 -0
  97. package/dist/ui-kit/repeated-slots.d.ts.map +1 -0
  98. package/dist/ui-kit/repeated-slots.js +81 -0
  99. package/dist/ui-kit/repeated-slots.js.map +1 -0
  100. package/dist/ui-kit/sections/accordion.d.ts +4 -1
  101. package/dist/ui-kit/sections/accordion.d.ts.map +1 -1
  102. package/dist/ui-kit/sections/accordion.js +2 -2
  103. package/dist/ui-kit/sections/accordion.js.map +1 -1
  104. package/dist/ui-kit/sections/contact-block.d.ts +4 -1
  105. package/dist/ui-kit/sections/contact-block.d.ts.map +1 -1
  106. package/dist/ui-kit/sections/contact-block.js +3 -3
  107. package/dist/ui-kit/sections/contact-block.js.map +1 -1
  108. package/dist/ui-kit/sections/faqs.d.ts +4 -1
  109. package/dist/ui-kit/sections/faqs.d.ts.map +1 -1
  110. package/dist/ui-kit/sections/faqs.js +4 -5
  111. package/dist/ui-kit/sections/faqs.js.map +1 -1
  112. package/dist/ui-kit/sections/hero.d.ts +113 -0
  113. package/dist/ui-kit/sections/hero.d.ts.map +1 -0
  114. package/dist/ui-kit/sections/hero.js +58 -0
  115. package/dist/ui-kit/sections/hero.js.map +1 -0
  116. package/dist/ui-kit/sections/index.d.ts +2 -0
  117. package/dist/ui-kit/sections/index.d.ts.map +1 -1
  118. package/dist/ui-kit/sections/index.js +2 -0
  119. package/dist/ui-kit/sections/index.js.map +1 -1
  120. package/dist/ui-kit/sections/price-guide.d.ts +4 -1
  121. package/dist/ui-kit/sections/price-guide.d.ts.map +1 -1
  122. package/dist/ui-kit/sections/price-guide.js +3 -3
  123. package/dist/ui-kit/sections/price-guide.js.map +1 -1
  124. package/dist/ui-kit/sections/price-table.d.ts +7 -9
  125. package/dist/ui-kit/sections/price-table.d.ts.map +1 -1
  126. package/dist/ui-kit/sections/price-table.js +8 -8
  127. package/dist/ui-kit/sections/price-table.js.map +1 -1
  128. package/dist/ui-kit/sections/proof-strip.d.ts +4 -1
  129. package/dist/ui-kit/sections/proof-strip.d.ts.map +1 -1
  130. package/dist/ui-kit/sections/proof-strip.js +2 -2
  131. package/dist/ui-kit/sections/proof-strip.js.map +1 -1
  132. package/dist/ui-kit/sections/reviews-band.d.ts +5 -2
  133. package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -1
  134. package/dist/ui-kit/sections/reviews-band.js +2 -2
  135. package/dist/ui-kit/sections/reviews-band.js.map +1 -1
  136. package/dist/ui-kit/sections/services-grid.d.ts +82 -0
  137. package/dist/ui-kit/sections/services-grid.d.ts.map +1 -0
  138. package/dist/ui-kit/sections/services-grid.js +90 -0
  139. package/dist/ui-kit/sections/services-grid.js.map +1 -0
  140. package/dist/ui-kit/sections/site-footer.d.ts +4 -1
  141. package/dist/ui-kit/sections/site-footer.d.ts.map +1 -1
  142. package/dist/ui-kit/sections/site-footer.js +6 -6
  143. package/dist/ui-kit/sections/site-footer.js.map +1 -1
  144. package/dist/ui-kit/shell/mobile-drawer.d.ts +7 -3
  145. package/dist/ui-kit/shell/mobile-drawer.d.ts.map +1 -1
  146. package/dist/ui-kit/shell/mobile-drawer.js +20 -4
  147. package/dist/ui-kit/shell/mobile-drawer.js.map +1 -1
  148. package/dist/ui-kit/shell/site-header.d.ts +4 -1
  149. package/dist/ui-kit/shell/site-header.d.ts.map +1 -1
  150. package/dist/ui-kit/shell/site-header.js +8 -8
  151. package/dist/ui-kit/shell/site-header.js.map +1 -1
  152. package/dist/ui-kit/shell/social-links.d.ts +1 -2
  153. package/dist/ui-kit/shell/social-links.d.ts.map +1 -1
  154. package/dist/ui-kit/shell/social-links.js +6 -3
  155. package/dist/ui-kit/shell/social-links.js.map +1 -1
  156. package/dist/ui-kit/shell/utility-bar.d.ts +4 -1
  157. package/dist/ui-kit/shell/utility-bar.d.ts.map +1 -1
  158. package/dist/ui-kit/shell/utility-bar.js +2 -2
  159. package/dist/ui-kit/shell/utility-bar.js.map +1 -1
  160. package/dist/ui-kit/skin-index.d.ts +2 -1
  161. package/dist/ui-kit/skin-index.d.ts.map +1 -1
  162. package/dist/ui-kit/skin-index.js +2 -1
  163. package/dist/ui-kit/skin-index.js.map +1 -1
  164. package/dist/ui-kit/skin.d.ts +21 -4
  165. package/dist/ui-kit/skin.d.ts.map +1 -1
  166. package/dist/ui-kit/skin.js +14 -1
  167. package/dist/ui-kit/skin.js.map +1 -1
  168. package/dist/ui-kit/structure.css +383 -3
  169. package/package.json +6 -2
@@ -1,17 +1,27 @@
1
1
  /**
2
2
  * The skin model of the Brandlab ui-kit. A site gives every shared component its own look through one skin entry per
3
- * component: a variant from the component's closed list, typed layout options and one class string per named slot.
3
+ * component: a variant from the component's closed list, typed layout options and one class string per one-off slot.
4
4
  * Colours, fonts and shapes come from the site's `--bl-*` tokens and these classes; behaviour, semantics, accessible
5
5
  * names and analytics hooks never appear here, so a skin can't change them.
6
+ *
7
+ * Repeated slots (3.14.0): a part the component renders once per entry of a list in its data (menu and panel links,
8
+ * footer links, drawer links, reviews, stars, cards, chips, list items) carries its `data-slot` hook and no class. The
9
+ * site styles it once in `src/site/skin.css` through the hooks, `[data-bl="footer"] [data-slot="link"] { … }`, so its
10
+ * class string is not repeated in the HTML and the RSC payload for every item. Each component lists them in its
11
+ * `X_REPEATED_SLOTS`; the skin entry's `classes` type leaves them out.
6
12
  */
7
13
  import { type ComponentType } from 'react';
8
14
  /** One class string per named slot. Tailwind v4 utilities, CSS Module class names or nothing (plain CSS on hooks). */
9
15
  export type SlotClasses<Slot extends string> = Readonly<Partial<Record<Slot, string>>>;
10
- /** A component's skin: its variant, its layout options and its slot classes. Every field is optional. */
11
- export interface SkinEntry<Slot extends string, Variant extends string = never, Layout extends object = Record<never, never>> {
16
+ /**
17
+ * A component's skin: its variant, its layout options and its slot classes. Every field is optional. `Repeated` names
18
+ * the slots the component renders once per list entry; they take no class here (style them from the skin stylesheet
19
+ * through their `data-slot` hooks).
20
+ */
21
+ export interface SkinEntry<Slot extends string, Variant extends string = never, Layout extends object = Record<never, never>, Repeated extends Slot = never> {
12
22
  readonly variant?: Variant;
13
23
  readonly layout?: Readonly<Layout>;
14
- readonly classes?: SlotClasses<Slot>;
24
+ readonly classes?: SlotClasses<Exclude<Slot, Repeated>>;
15
25
  }
16
26
  type SkinKeys = 'variant' | 'layout' | 'classes';
17
27
  /** The part of a component's props a skin entry supplies. */
@@ -35,6 +45,13 @@ export declare function skinned<P extends {
35
45
  }>(Component: ComponentType<P>, entry: SkinEntryOf<P>): ComponentType<SkinnedProps<P>>;
36
46
  /** Join class strings, dropping empty parts. Returns `undefined` when nothing is left, so no empty `class` renders. */
37
47
  export declare function cx(...parts: readonly (string | false | null | undefined)[]): string | undefined;
48
+ /**
49
+ * The hook for a repeated part (3.14.0): `data-slot` only, never a class. Spread it onto each item:
50
+ * `<li {...repeatedSlot('item')}>`. A site styles it from its skin stylesheet through `[data-bl] [data-slot]`.
51
+ */
52
+ export declare function repeatedSlot<Slot extends string>(name: Slot): {
53
+ 'data-slot': Slot;
54
+ };
38
55
  /**
39
56
  * The stable hook for one named part: `data-slot` plus the skin's class for it. Spread it onto the element:
40
57
  * `<div {...slot(classes, 'inner')}>`. Slot names, nesting and element types are public API (a minor release to
@@ -1 +1 @@
1
- {"version":3,"file":"skin.d.ts","sourceRoot":"","sources":["../../src/ui-kit/skin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAiB,KAAK,aAAa,EAAkB,MAAM,OAAO,CAAA;AAEzE,sHAAsH;AACtH,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAEtF,yGAAyG;AACzG,MAAM,WAAW,SAAS,CACxB,IAAI,SAAS,MAAM,EACnB,OAAO,SAAS,MAAM,GAAG,KAAK,EAC9B,MAAM,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC;IAE5C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;CACrC;AAED,KAAK,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEhD,6DAA6D;AAC7D,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAA;AAEhE,kHAAkH;AAClH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC,CAAA;AAE5D;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EACxF,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,EAC3B,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GACpB,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAehC;AAED,uHAAuH;AACvH,wBAAgB,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,GAAG,SAAS,CAG/F;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,IAAI,SAAS,MAAM,EACtC,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,SAAS,EACtC,IAAI,EAAE,IAAI,EACV,KAAK,CAAC,EAAE,MAAM,GACb;IAAE,WAAW,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAEtD"}
1
+ {"version":3,"file":"skin.d.ts","sourceRoot":"","sources":["../../src/ui-kit/skin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAiB,KAAK,aAAa,EAAkB,MAAM,OAAO,CAAA;AAEzE,sHAAsH;AACtH,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;AAEtF;;;;GAIG;AACH,MAAM,WAAW,SAAS,CACxB,IAAI,SAAS,MAAM,EACnB,OAAO,SAAS,MAAM,GAAG,KAAK,EAC9B,MAAM,SAAS,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAC5C,QAAQ,SAAS,IAAI,GAAG,KAAK;IAE7B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAA;CACxD;AAED,KAAK,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEhD,6DAA6D;AAC7D,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAA;AAEhE,kHAAkH;AAClH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC,CAAA;AAE5D;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EACxF,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,EAC3B,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,GACpB,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAehC;AAED,uHAAuH;AACvH,wBAAgB,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,CAAC,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,GAAG,SAAS,CAG/F;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,SAAS,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,IAAI,SAAS,MAAM,EACtC,OAAO,EAAE,WAAW,CAAC,IAAI,CAAC,GAAG,SAAS,EACtC,IAAI,EAAE,IAAI,EACV,KAAK,CAAC,EAAE,MAAM,GACb;IAAE,WAAW,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAEtD"}
@@ -1,8 +1,14 @@
1
1
  /**
2
2
  * The skin model of the Brandlab ui-kit. A site gives every shared component its own look through one skin entry per
3
- * component: a variant from the component's closed list, typed layout options and one class string per named slot.
3
+ * component: a variant from the component's closed list, typed layout options and one class string per one-off slot.
4
4
  * Colours, fonts and shapes come from the site's `--bl-*` tokens and these classes; behaviour, semantics, accessible
5
5
  * names and analytics hooks never appear here, so a skin can't change them.
6
+ *
7
+ * Repeated slots (3.14.0): a part the component renders once per entry of a list in its data (menu and panel links,
8
+ * footer links, drawer links, reviews, stars, cards, chips, list items) carries its `data-slot` hook and no class. The
9
+ * site styles it once in `src/site/skin.css` through the hooks, `[data-bl="footer"] [data-slot="link"] { … }`, so its
10
+ * class string is not repeated in the HTML and the RSC payload for every item. Each component lists them in its
11
+ * `X_REPEATED_SLOTS`; the skin entry's `classes` type leaves them out.
6
12
  */
7
13
  import { createElement } from 'react';
8
14
  /**
@@ -36,6 +42,13 @@ export function cx(...parts) {
36
42
  const joined = parts.filter(Boolean).join(' ');
37
43
  return joined === '' ? undefined : joined;
38
44
  }
45
+ /**
46
+ * The hook for a repeated part (3.14.0): `data-slot` only, never a class. Spread it onto each item:
47
+ * `<li {...repeatedSlot('item')}>`. A site styles it from its skin stylesheet through `[data-bl] [data-slot]`.
48
+ */
49
+ export function repeatedSlot(name) {
50
+ return { 'data-slot': name };
51
+ }
39
52
  /**
40
53
  * The stable hook for one named part: `data-slot` plus the skin's class for it. Spread it onto the element:
41
54
  * `<div {...slot(classes, 'inner')}>`. Slot names, nesting and element types are public API (a minor release to
@@ -1 +1 @@
1
- {"version":3,"file":"skin.js","sourceRoot":"","sources":["../../src/ui-kit/skin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAAsC,MAAM,OAAO,CAAA;AAwBzE;;;;;;;;;GASG;AACH,MAAM,UAAU,OAAO,CACrB,SAA2B,EAC3B,KAAqB;IAErB,MAAM,IAAI,GAAG,KAAiE,CAAA;IAC9E,SAAS,OAAO,CAAC,KAAsB;QACrC,MAAM,UAAU,GAAI,KAA6B,CAAC,MAAM,CAAA;QACxD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QACxG,MAAM,MAAM,GAAG;YACb,GAAG,KAAK;YACR,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnC,CAAA;QACjB,OAAO,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IACD,OAAO,CAAC,WAAW,GAAG,WAAW,SAAS,CAAC,WAAW,IAAI,SAAS,CAAC,IAAI,IAAI,WAAW,GAAG,CAAA;IAC1F,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,EAAE,CAAC,GAAG,KAAqD;IACzE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9C,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAA;AAC3C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAClB,OAAsC,EACtC,IAAU,EACV,KAAc;IAEd,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,EAAE,CAAA;AACrE,CAAC","sourcesContent":["/**\n * The skin model of the Brandlab ui-kit. A site gives every shared component its own look through one skin entry per\n * component: a variant from the component's closed list, typed layout options and one class string per named slot.\n * Colours, fonts and shapes come from the site's `--bl-*` tokens and these classes; behaviour, semantics, accessible\n * names and analytics hooks never appear here, so a skin can't change them.\n */\nimport { createElement, type ComponentType, type ReactNode } from 'react'\n\n/** One class string per named slot. Tailwind v4 utilities, CSS Module class names or nothing (plain CSS on hooks). */\nexport type SlotClasses<Slot extends string> = Readonly<Partial<Record<Slot, string>>>\n\n/** A component's skin: its variant, its layout options and its slot classes. Every field is optional. */\nexport interface SkinEntry<\n Slot extends string,\n Variant extends string = never,\n Layout extends object = Record<never, never>,\n> {\n readonly variant?: Variant\n readonly layout?: Readonly<Layout>\n readonly classes?: SlotClasses<Slot>\n}\n\ntype SkinKeys = 'variant' | 'layout' | 'classes'\n\n/** The part of a component's props a skin entry supplies. */\nexport type SkinEntryOf<P> = Pick<P, Extract<keyof P, SkinKeys>>\n\n/** What a page passes to a skinned component: its data, and at most a `layout` adjustment within the contract. */\nexport type SkinnedProps<P> = Omit<P, 'variant' | 'classes'>\n\n/**\n * Bind a component to the site's skin entry, so pages pass data only. Returns a plain function component (no\n * context, no client code), so it works in server components and client components alike. The skin's layout merges\n * under the page's `layout`; the variant and classes come from the skin only.\n *\n * ```tsx\n * // src/site/ui.tsx\n * export const ReviewsBand = skinned(UI.ReviewsBand, skin.reviewsBand)\n * ```\n */\nexport function skinned<P extends { variant?: unknown; layout?: object; classes?: object }>(\n Component: ComponentType<P>,\n entry: SkinEntryOf<P>,\n): ComponentType<SkinnedProps<P>> {\n const skin = entry as { variant?: unknown; layout?: object; classes?: object }\n function Skinned(props: SkinnedProps<P>): ReactNode {\n const pageLayout = (props as { layout?: object }).layout\n const layout = skin.layout || pageLayout ? { ...(skin.layout ?? {}), ...(pageLayout ?? {}) } : undefined\n const merged = {\n ...props,\n ...(skin.variant !== undefined ? { variant: skin.variant } : {}),\n ...(layout ? { layout } : {}),\n ...(skin.classes ? { classes: skin.classes } : {}),\n } as unknown as P\n return createElement(Component, merged)\n }\n Skinned.displayName = `Skinned(${Component.displayName ?? Component.name ?? 'Component'})`\n return Skinned\n}\n\n/** Join class strings, dropping empty parts. Returns `undefined` when nothing is left, so no empty `class` renders. */\nexport function cx(...parts: readonly (string | false | null | undefined)[]): string | undefined {\n const joined = parts.filter(Boolean).join(' ')\n return joined === '' ? undefined : joined\n}\n\n/**\n * The stable hook for one named part: `data-slot` plus the skin's class for it. Spread it onto the element:\n * `<div {...slot(classes, 'inner')}>`. Slot names, nesting and element types are public API (a minor release to\n * change); a skin's class never replaces the hook.\n */\nexport function slot<Slot extends string>(\n classes: SlotClasses<Slot> | undefined,\n name: Slot,\n extra?: string,\n): { 'data-slot': Slot; className: string | undefined } {\n return { 'data-slot': name, className: cx(classes?.[name], extra) }\n}\n"]}
1
+ {"version":3,"file":"skin.js","sourceRoot":"","sources":["../../src/ui-kit/skin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAsC,MAAM,OAAO,CAAA;AA6BzE;;;;;;;;;GASG;AACH,MAAM,UAAU,OAAO,CACrB,SAA2B,EAC3B,KAAqB;IAErB,MAAM,IAAI,GAAG,KAAiE,CAAA;IAC9E,SAAS,OAAO,CAAC,KAAsB;QACrC,MAAM,UAAU,GAAI,KAA6B,CAAC,MAAM,CAAA;QACxD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QACxG,MAAM,MAAM,GAAG;YACb,GAAG,KAAK;YACR,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnC,CAAA;QACjB,OAAO,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC;IACD,OAAO,CAAC,WAAW,GAAG,WAAW,SAAS,CAAC,WAAW,IAAI,SAAS,CAAC,IAAI,IAAI,WAAW,GAAG,CAAA;IAC1F,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,EAAE,CAAC,GAAG,KAAqD;IACzE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9C,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAA;AAC3C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAsB,IAAU;IAC1D,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAClB,OAAsC,EACtC,IAAU,EACV,KAAc;IAEd,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,EAAE,CAAA;AACrE,CAAC","sourcesContent":["/**\n * The skin model of the Brandlab ui-kit. A site gives every shared component its own look through one skin entry per\n * component: a variant from the component's closed list, typed layout options and one class string per one-off slot.\n * Colours, fonts and shapes come from the site's `--bl-*` tokens and these classes; behaviour, semantics, accessible\n * names and analytics hooks never appear here, so a skin can't change them.\n *\n * Repeated slots (3.14.0): a part the component renders once per entry of a list in its data (menu and panel links,\n * footer links, drawer links, reviews, stars, cards, chips, list items) carries its `data-slot` hook and no class. The\n * site styles it once in `src/site/skin.css` through the hooks, `[data-bl=\"footer\"] [data-slot=\"link\"] { … }`, so its\n * class string is not repeated in the HTML and the RSC payload for every item. Each component lists them in its\n * `X_REPEATED_SLOTS`; the skin entry's `classes` type leaves them out.\n */\nimport { createElement, type ComponentType, type ReactNode } from 'react'\n\n/** One class string per named slot. Tailwind v4 utilities, CSS Module class names or nothing (plain CSS on hooks). */\nexport type SlotClasses<Slot extends string> = Readonly<Partial<Record<Slot, string>>>\n\n/**\n * A component's skin: its variant, its layout options and its slot classes. Every field is optional. `Repeated` names\n * the slots the component renders once per list entry; they take no class here (style them from the skin stylesheet\n * through their `data-slot` hooks).\n */\nexport interface SkinEntry<\n Slot extends string,\n Variant extends string = never,\n Layout extends object = Record<never, never>,\n Repeated extends Slot = never,\n> {\n readonly variant?: Variant\n readonly layout?: Readonly<Layout>\n readonly classes?: SlotClasses<Exclude<Slot, Repeated>>\n}\n\ntype SkinKeys = 'variant' | 'layout' | 'classes'\n\n/** The part of a component's props a skin entry supplies. */\nexport type SkinEntryOf<P> = Pick<P, Extract<keyof P, SkinKeys>>\n\n/** What a page passes to a skinned component: its data, and at most a `layout` adjustment within the contract. */\nexport type SkinnedProps<P> = Omit<P, 'variant' | 'classes'>\n\n/**\n * Bind a component to the site's skin entry, so pages pass data only. Returns a plain function component (no\n * context, no client code), so it works in server components and client components alike. The skin's layout merges\n * under the page's `layout`; the variant and classes come from the skin only.\n *\n * ```tsx\n * // src/site/ui.tsx\n * export const ReviewsBand = skinned(UI.ReviewsBand, skin.reviewsBand)\n * ```\n */\nexport function skinned<P extends { variant?: unknown; layout?: object; classes?: object }>(\n Component: ComponentType<P>,\n entry: SkinEntryOf<P>,\n): ComponentType<SkinnedProps<P>> {\n const skin = entry as { variant?: unknown; layout?: object; classes?: object }\n function Skinned(props: SkinnedProps<P>): ReactNode {\n const pageLayout = (props as { layout?: object }).layout\n const layout = skin.layout || pageLayout ? { ...(skin.layout ?? {}), ...(pageLayout ?? {}) } : undefined\n const merged = {\n ...props,\n ...(skin.variant !== undefined ? { variant: skin.variant } : {}),\n ...(layout ? { layout } : {}),\n ...(skin.classes ? { classes: skin.classes } : {}),\n } as unknown as P\n return createElement(Component, merged)\n }\n Skinned.displayName = `Skinned(${Component.displayName ?? Component.name ?? 'Component'})`\n return Skinned\n}\n\n/** Join class strings, dropping empty parts. Returns `undefined` when nothing is left, so no empty `class` renders. */\nexport function cx(...parts: readonly (string | false | null | undefined)[]): string | undefined {\n const joined = parts.filter(Boolean).join(' ')\n return joined === '' ? undefined : joined\n}\n\n/**\n * The hook for a repeated part (3.14.0): `data-slot` only, never a class. Spread it onto each item:\n * `<li {...repeatedSlot('item')}>`. A site styles it from its skin stylesheet through `[data-bl] [data-slot]`.\n */\nexport function repeatedSlot<Slot extends string>(name: Slot): { 'data-slot': Slot } {\n return { 'data-slot': name }\n}\n\n/**\n * The stable hook for one named part: `data-slot` plus the skin's class for it. Spread it onto the element:\n * `<div {...slot(classes, 'inner')}>`. Slot names, nesting and element types are public API (a minor release to\n * change); a skin's class never replaces the hook.\n */\nexport function slot<Slot extends string>(\n classes: SlotClasses<Slot> | undefined,\n name: Slot,\n extra?: string,\n): { 'data-slot': Slot; className: string | undefined } {\n return { 'data-slot': name, className: cx(classes?.[name], extra) }\n}\n"]}
@@ -97,11 +97,10 @@
97
97
 
98
98
  [data-bl='stars'] {
99
99
  display: inline-flex;
100
- gap: 0.125rem;
101
100
  color: var(--bl-color-star, #f5b301);
102
101
  }
103
- [data-bl='stars'] [data-slot='star'] svg {
104
- width: 1em;
102
+ [data-bl='stars'] > svg {
103
+ width: 5.4em;
105
104
  height: 1em;
106
105
  }
107
106
  [data-bl='stars'] [data-slot='star'][data-state='off'] {
@@ -120,6 +119,198 @@
120
119
  }
121
120
  }
122
121
 
122
+ /* 05-media.css */
123
+ /* MediaFrame and Media (F13), the library's one image path: the frame holds the image at its own shape, or cropped to
124
+ an allowed ratio around the focal point (inline object-position); a caption flows under it. An AI image's
125
+ "Illustration" label (inside the figcaption) sits over the image's top corner; the site colours it. */
126
+
127
+ [data-bl='media'] {
128
+ position: relative;
129
+ margin: 0;
130
+ }
131
+ [data-bl='media'] > img {
132
+ display: block;
133
+ max-width: 100%;
134
+ height: auto;
135
+ }
136
+ [data-bl='media'][data-fit='fluid'] > img {
137
+ width: 100%;
138
+ }
139
+ [data-bl='media'][data-ratio] > img {
140
+ width: 100%;
141
+ height: 100%;
142
+ object-fit: cover;
143
+ }
144
+ [data-bl='media'][data-ratio='16:9'] > img {
145
+ aspect-ratio: 16 / 9;
146
+ }
147
+ [data-bl='media'][data-ratio='4:5'] > img {
148
+ aspect-ratio: 4 / 5;
149
+ }
150
+ [data-bl='media'][data-ratio='4:3'] > img {
151
+ aspect-ratio: 4 / 3;
152
+ }
153
+ [data-bl='media'][data-ratio='3:2'] > img {
154
+ aspect-ratio: 3 / 2;
155
+ }
156
+ [data-bl='media'] [data-slot='label'] {
157
+ position: absolute;
158
+ inset-block-start: 0.5rem;
159
+ inset-inline-start: 0.5rem;
160
+ padding: 0.125rem 0.5rem;
161
+ background: var(--bl-color-surface, Canvas);
162
+ color: var(--bl-color-ink, CanvasText);
163
+ font-size: 0.8125rem;
164
+ line-height: 1.4;
165
+ }
166
+ /* Behind the page header's text, the figure fills its media slot. */
167
+ :where([data-bl-section] [data-slot='media']) > [data-bl='media'][data-ratio] {
168
+ height: 100%;
169
+ }
170
+
171
+ /* 05-primitives.css */
172
+ /* Primitives (3.14.0): Button, Eyebrow, TickList, LinkList, Card, Badge and Chip, Stat, MediaFrame. Layout only. */
173
+
174
+ button[data-bl='button'] {
175
+ font: inherit;
176
+ cursor: pointer;
177
+ }
178
+ button[data-bl='button']:disabled {
179
+ cursor: not-allowed;
180
+ }
181
+
182
+ [data-bl='eyebrow'] {
183
+ margin: 0;
184
+ }
185
+
186
+ /* TickList: one column, or two from the first width up (the hero checklist). */
187
+ [data-bl='tick-list'] {
188
+ margin: 0;
189
+ padding: 0;
190
+ list-style: none;
191
+ display: grid;
192
+ gap: 0.5rem 1.5rem;
193
+ }
194
+ [data-bl='tick-list'][data-columns='2'] {
195
+ grid-template-columns: repeat(2, minmax(0, 1fr));
196
+ }
197
+ [data-bl='tick-list'] [data-slot='item'] {
198
+ display: flex;
199
+ align-items: flex-start;
200
+ gap: 0.5rem;
201
+ }
202
+ [data-bl='tick-list'] [data-slot='tick'] {
203
+ color: var(--bl-color-action, currentColor);
204
+ }
205
+ [data-bl='tick-list'] [data-slot='tick'] svg {
206
+ width: 1.25em;
207
+ height: 1.25em;
208
+ }
209
+ /* tick: 'rule': no svg, a short rule on the first line's centre (AA's ruled lists). */
210
+ [data-bl='tick-list'][data-tick='rule'] [data-slot='tick'] {
211
+ flex: none;
212
+ width: 0.875em;
213
+ height: 2px;
214
+ margin-top: 0.7em;
215
+ background: currentColor;
216
+ }
217
+
218
+ /* LinkList: plain entries, one column on phones, two from 40rem, the chosen count from 64rem. */
219
+ [data-bl='link-list'] {
220
+ margin: 0;
221
+ padding: 0;
222
+ list-style: none;
223
+ display: grid;
224
+ column-gap: 2.5rem;
225
+ }
226
+ [data-bl='link-list'] > li > a {
227
+ display: flex;
228
+ align-items: center;
229
+ min-height: 44px;
230
+ }
231
+ @media (min-width: 40rem) {
232
+ [data-bl='link-list']:not([data-columns='1']) {
233
+ grid-template-columns: repeat(2, minmax(0, 1fr));
234
+ }
235
+ }
236
+ @media (min-width: 64rem) {
237
+ [data-bl='link-list'][data-columns='3'] {
238
+ grid-template-columns: repeat(3, minmax(0, 1fr));
239
+ }
240
+ [data-bl='link-list'][data-columns='4'] {
241
+ grid-template-columns: repeat(4, minmax(0, 1fr));
242
+ }
243
+ }
244
+
245
+ /* Card: media then body; a linked card is one hit area through its title link. */
246
+ [data-bl='card'] {
247
+ position: relative;
248
+ display: flex;
249
+ flex-direction: column;
250
+ }
251
+ [data-bl='card'] [data-slot='title'] {
252
+ margin: 0;
253
+ }
254
+ [data-bl='card'] [data-slot='link']::after {
255
+ content: '';
256
+ position: absolute;
257
+ inset: 0;
258
+ }
259
+ @media (min-width: 40rem) {
260
+ [data-bl='card'][data-media='start'] {
261
+ flex-direction: row;
262
+ }
263
+ [data-bl='card'][data-media='start'] > [data-slot='media'] {
264
+ flex: 0 0 40%;
265
+ }
266
+ }
267
+
268
+ [data-bl='badge'],
269
+ [data-bl='chip'] {
270
+ display: inline-flex;
271
+ align-items: center;
272
+ gap: 0.375em;
273
+ }
274
+ :where([data-bl='badge'], [data-bl='chip']) [data-slot='icon'] svg {
275
+ width: 1em;
276
+ height: 1em;
277
+ }
278
+
279
+ [data-bl='stat'] {
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 0.25rem;
283
+ }
284
+ [data-bl='stat'] [data-slot='link'] {
285
+ display: inline-flex;
286
+ align-items: center;
287
+ min-height: 44px;
288
+ }
289
+
290
+ /* 06-breadcrumbs.css */
291
+ /* Breadcrumbs (F11): one wrapping line; the separator is drawn here (no element per crumb) with empty alternative
292
+ text, so assistive technology reads the list alone. A skin may restyle or replace it on the same selector. */
293
+
294
+ [data-bl='breadcrumbs'] [data-slot='list'] {
295
+ display: flex;
296
+ flex-wrap: wrap;
297
+ align-items: center;
298
+ gap: 0.25rem 0.5rem;
299
+ margin: 0;
300
+ padding: 0;
301
+ list-style: none;
302
+ }
303
+ [data-bl='breadcrumbs'] [data-slot='item'] {
304
+ display: inline-flex;
305
+ align-items: center;
306
+ gap: 0.5rem;
307
+ }
308
+ [data-bl='breadcrumbs'] [data-slot='item'] + [data-slot='item']::before {
309
+ content: '/';
310
+ content: '/' / '';
311
+ opacity: 0.6;
312
+ }
313
+
123
314
  /* 10-header.css */
124
315
  /*
125
316
  * F2 header and F3 panels: sticky and solid, modes by the header's own width, panels always in the HTML. The drawer's
@@ -911,6 +1102,103 @@
911
1102
  padding: 1rem var(--bl-gutter, 1.25rem) var(--bl-gutter, 1.25rem);
912
1103
  }
913
1104
 
1105
+ /* 29-hero.css */
1106
+ /* S1 Hero (3.14.0): layout only. Phones stack in source order (text, actions, checklist, photo at 4:3, form). From
1107
+ 1024 px of the hero's own width, `split` puts the text and form on one side and the photo on the other, and
1108
+ `full-bleed` puts the photo behind both, under the skin's scrim, with the form as a card beside the text. */
1109
+
1110
+ [data-bl-section='hero'] {
1111
+ container: bl-hero / inline-size;
1112
+ isolation: isolate;
1113
+ }
1114
+ [data-bl-section='hero'] :where(h1, p) {
1115
+ margin: 0;
1116
+ }
1117
+ [data-bl-section='hero'] > [data-slot='inner'] {
1118
+ display: grid;
1119
+ gap: 1.5rem;
1120
+ }
1121
+ [data-bl-section='hero'] [data-slot='content'] {
1122
+ display: grid;
1123
+ align-content: start;
1124
+ gap: 1rem;
1125
+ }
1126
+ [data-bl-section='hero'] [data-slot='actions'] {
1127
+ display: flex;
1128
+ flex-wrap: wrap;
1129
+ align-items: center;
1130
+ gap: 0.75rem;
1131
+ }
1132
+ [data-bl-section='hero'] [data-slot='scrim'] {
1133
+ display: none;
1134
+ }
1135
+ /* The root is the bl-hero container, and a container never matches its own query, so the full-bleed root is set up
1136
+ here: it holds the absolute photo and scrim from 1024 px. */
1137
+ [data-bl-section='hero'][data-variant='full-bleed'] {
1138
+ position: relative;
1139
+ overflow: hidden;
1140
+ }
1141
+
1142
+ @container bl-hero (min-width: 1024px) {
1143
+ [data-bl-section='hero'][data-height='tall'] > [data-slot='inner'] {
1144
+ padding-block: calc(var(--bl-space-section, 4rem) * 1.5);
1145
+ }
1146
+ [data-bl-section='hero'] > [data-slot='inner'] {
1147
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
1148
+ grid-template-areas:
1149
+ 'content media'
1150
+ 'form media';
1151
+ grid-template-rows: auto 1fr;
1152
+ column-gap: 3rem;
1153
+ align-items: start;
1154
+ }
1155
+ [data-bl-section='hero'][data-media-side='start'] > [data-slot='inner'] {
1156
+ grid-template-areas:
1157
+ 'media content'
1158
+ 'media form';
1159
+ }
1160
+ [data-bl-section='hero'] [data-slot='content'] {
1161
+ grid-area: content;
1162
+ }
1163
+ [data-bl-section='hero'] > [data-slot='inner'] > [data-slot='media'] {
1164
+ grid-area: media;
1165
+ align-self: stretch;
1166
+ }
1167
+ [data-bl-section='hero'] > [data-slot='inner'] > [data-slot='form'] {
1168
+ grid-area: form;
1169
+ }
1170
+ /* The photo fills its side: no white gaps beside it (Romie, Gabrael and H&R). */
1171
+ [data-bl-section='hero'] [data-slot='media'] > [data-bl='media'] {
1172
+ height: 100%;
1173
+ }
1174
+ [data-bl-section='hero'] [data-slot='media'] > [data-bl='media'] > img {
1175
+ height: 100%;
1176
+ min-height: 24rem;
1177
+ aspect-ratio: auto;
1178
+ }
1179
+
1180
+ /* full-bleed: the photo behind the whole hero; text and the form card side by side over it. */
1181
+ [data-bl-section='hero'][data-variant='full-bleed'] > [data-slot='inner'] {
1182
+ position: static;
1183
+ grid-template-columns: minmax(0, 1fr) minmax(0, 28rem);
1184
+ grid-template-areas: 'content form';
1185
+ grid-template-rows: auto;
1186
+ }
1187
+ [data-bl-section='hero'][data-variant='full-bleed'] > [data-slot='inner'] > [data-slot='media'] {
1188
+ position: absolute;
1189
+ inset: 0;
1190
+ z-index: -1;
1191
+ }
1192
+ [data-bl-section='hero'][data-variant='full-bleed'] [data-slot='media'] > [data-bl='media'] > img {
1193
+ min-height: 0;
1194
+ }
1195
+ [data-bl-section='hero'][data-variant='full-bleed'] [data-slot='scrim'] {
1196
+ display: block;
1197
+ position: absolute;
1198
+ inset: 0;
1199
+ }
1200
+ }
1201
+
914
1202
  /* 30-reviews.css */
915
1203
  /* S7 reviews band and the rating chip: layout only. Colours, fonts and shapes come from each site's skin. */
916
1204
 
@@ -1480,4 +1768,96 @@
1480
1768
  color: var(--bl-color-on-band, inherit);
1481
1769
  }
1482
1770
 
1771
+ /* 39-services.css */
1772
+ /* Services grid (S4). `rows`: text beside the photo from 1024 px of its own width, the photo alternating sides by
1773
+ default. `cards`: a grid of up to 4 columns, the photo on top. `compact`: a lean, wrapping list of links. Lists
1774
+ styled without bullets carry role="list", so Safari keeps announcing them as lists. */
1775
+
1776
+ [data-bl-section='services'] {
1777
+ container: bl-services / inline-size;
1778
+ }
1779
+ [data-bl-section='services'] [data-slot='list'] {
1780
+ margin: 0;
1781
+ padding: 0;
1782
+ list-style: none;
1783
+ }
1784
+ [data-bl-section='services'] [data-slot='list'] [data-slot='points'] {
1785
+ margin: 0;
1786
+ padding: 0;
1787
+ list-style: none;
1788
+ }
1789
+ [data-bl-section='services'] [data-slot='list'] [data-slot='title'] {
1790
+ margin: 0;
1791
+ }
1792
+
1793
+ /* Rows: text first in reading order; the photo above it on phones. */
1794
+ [data-bl-section='services'][data-variant='rows'] [data-slot='list'] > [data-slot='item'] {
1795
+ display: grid;
1796
+ gap: 1.25rem;
1797
+ padding-block: 2rem;
1798
+ }
1799
+ [data-bl-section='services'][data-variant='rows'] [data-slot='list'] > [data-slot='item'] > [data-slot='media'] {
1800
+ order: -1;
1801
+ }
1802
+
1803
+ /* Cards: the photo on top. */
1804
+ [data-bl-section='services'][data-variant='cards'] [data-slot='list'] {
1805
+ display: grid;
1806
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
1807
+ gap: var(--bl-gutter, 1.25rem);
1808
+ }
1809
+ [data-bl-section='services'][data-variant='cards'] [data-slot='list'] > [data-slot='item'] {
1810
+ display: flex;
1811
+ flex-direction: column;
1812
+ gap: 0.75rem;
1813
+ }
1814
+ [data-bl-section='services'][data-variant='cards'] [data-slot='list'] > [data-slot='item'] > [data-slot='media'] {
1815
+ order: -1;
1816
+ }
1817
+
1818
+ /* Compact: every service as one link, in columns. */
1819
+ [data-bl-section='services'][data-variant='compact'] [data-slot='list'] {
1820
+ display: grid;
1821
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
1822
+ gap: 0 var(--bl-gutter, 1.25rem);
1823
+ }
1824
+ [data-bl-section='services'][data-variant='compact'] [data-slot='list'] > [data-slot='item'] > [data-slot='link'] {
1825
+ display: flex;
1826
+ align-items: center;
1827
+ min-height: 44px;
1828
+ }
1829
+
1830
+ @container bl-services (min-width: 1024px) {
1831
+ [data-bl-section='services'][data-variant='rows'] [data-slot='list'] > [data-slot='item']:has(> [data-slot='media']) {
1832
+ grid-template-columns: 1fr 1fr;
1833
+ align-items: center;
1834
+ column-gap: clamp(3rem, 6cqi, 5rem);
1835
+ }
1836
+ [data-bl-section='services'][data-variant='rows'] [data-slot='list'] > [data-slot='item'] > [data-slot='media'] {
1837
+ grid-row: 1;
1838
+ grid-column: 1;
1839
+ }
1840
+ [data-bl-section='services'][data-variant='rows'] [data-slot='list'] > [data-slot='item']:has(> [data-slot='media']) > [data-slot='body'] {
1841
+ grid-row: 1;
1842
+ grid-column: 2;
1843
+ }
1844
+ [data-bl-section='services'][data-variant='rows'][data-media-side='end'] [data-slot='list'] > [data-slot='item'] > [data-slot='media'],
1845
+ [data-bl-section='services'][data-variant='rows'][data-media-side='alternate'] [data-slot='list'] > [data-slot='item']:nth-child(even) > [data-slot='media'] {
1846
+ grid-column: 2;
1847
+ }
1848
+ [data-bl-section='services'][data-variant='rows'][data-media-side='end'] [data-slot='list'] > [data-slot='item']:has(> [data-slot='media']) > [data-slot='body'],
1849
+ [data-bl-section='services'][data-variant='rows'][data-media-side='alternate'] [data-slot='list'] > [data-slot='item']:nth-child(even):has(> [data-slot='media']) > [data-slot='body'] {
1850
+ grid-column: 1;
1851
+ }
1852
+ [data-bl-section='services'][data-variant='cards'][data-columns='2'] [data-slot='list'] {
1853
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1854
+ }
1855
+ [data-bl-section='services'][data-variant='cards'][data-columns='3'] [data-slot='list'] {
1856
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1857
+ }
1858
+ [data-bl-section='services'][data-variant='cards'][data-columns='4'] [data-slot='list'] {
1859
+ grid-template-columns: repeat(4, minmax(0, 1fr));
1860
+ }
1861
+ }
1862
+
1483
1863
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandlab-digital/site-next",
3
- "version": "3.13.0",
3
+ "version": "3.14.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.13.0",
28
+ "@brandlab-digital/site-core": "3.14.0",
29
29
  "browser-image-compression": "2.0.2",
30
30
  "pdf-lib": "1.17.1",
31
31
  "server-only": "0.0.1",
@@ -162,6 +162,10 @@
162
162
  "types": "./dist/ui-kit/skin-index.d.ts",
163
163
  "import": "./dist/ui-kit/skin-index.js"
164
164
  },
165
+ "./ui-kit/icons": {
166
+ "types": "./dist/ui-kit/icon-glyphs.d.ts",
167
+ "import": "./dist/ui-kit/icon-glyphs.js"
168
+ },
165
169
  "./ui-kit/primitives": {
166
170
  "types": "./dist/ui-kit/primitives/index.d.ts",
167
171
  "import": "./dist/ui-kit/primitives/index.js"