@brandlab-digital/site-next 3.7.0 → 3.8.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 (38) hide show
  1. package/dist/ui-kit/brandlab-skin.d.ts +17 -0
  2. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -0
  3. package/dist/ui-kit/brandlab-skin.js +2 -0
  4. package/dist/ui-kit/brandlab-skin.js.map +1 -0
  5. package/dist/ui-kit/icons.d.ts +15 -0
  6. package/dist/ui-kit/icons.d.ts.map +1 -0
  7. package/dist/ui-kit/icons.js +72 -0
  8. package/dist/ui-kit/icons.js.map +1 -0
  9. package/dist/ui-kit/primitives/index.d.ts +6 -0
  10. package/dist/ui-kit/primitives/index.d.ts.map +1 -0
  11. package/dist/ui-kit/primitives/index.js +6 -0
  12. package/dist/ui-kit/primitives/index.js.map +1 -0
  13. package/dist/ui-kit/primitives/layout.d.ts +73 -0
  14. package/dist/ui-kit/primitives/layout.d.ts.map +1 -0
  15. package/dist/ui-kit/primitives/layout.js +34 -0
  16. package/dist/ui-kit/primitives/layout.js.map +1 -0
  17. package/dist/ui-kit/primitives/links.d.ts +48 -0
  18. package/dist/ui-kit/primitives/links.d.ts.map +1 -0
  19. package/dist/ui-kit/primitives/links.js +36 -0
  20. package/dist/ui-kit/primitives/links.js.map +1 -0
  21. package/dist/ui-kit/primitives/quote-button.d.ts +20 -0
  22. package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -0
  23. package/dist/ui-kit/primitives/quote-button.js +15 -0
  24. package/dist/ui-kit/primitives/quote-button.js.map +1 -0
  25. package/dist/ui-kit/skin-index.d.ts +3 -0
  26. package/dist/ui-kit/skin-index.d.ts.map +1 -0
  27. package/dist/ui-kit/skin-index.js +2 -0
  28. package/dist/ui-kit/skin-index.js.map +1 -0
  29. package/dist/ui-kit/skin.d.ts +48 -0
  30. package/dist/ui-kit/skin.d.ts.map +1 -0
  31. package/dist/ui-kit/skin.js +47 -0
  32. package/dist/ui-kit/skin.js.map +1 -0
  33. package/dist/ui-kit/structure.css +123 -0
  34. package/dist/ui-kit/types.d.ts +27 -0
  35. package/dist/ui-kit/types.d.ts.map +1 -0
  36. package/dist/ui-kit/types.js +6 -0
  37. package/dist/ui-kit/types.js.map +1 -0
  38. package/package.json +17 -4
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A site's whole look for the ui-kit: one entry per shared component it renders (`src/site/skin.ts`, written with
3
+ * `satisfies BrandlabSkin`). Each release adds the entries for its components. K2 checks that every rendered
4
+ * component has an entry and that no reference-skin class ships.
5
+ */
6
+ import type { ButtonLinkSkin, PhoneLinkSkin, TextLinkSkin } from './primitives/links.js';
7
+ import type { QuoteButtonSkin } from './primitives/quote-button.js';
8
+ import type { ContainerSkin, SectionHeadingSkin } from './primitives/layout.js';
9
+ export interface BrandlabSkin {
10
+ readonly buttonLink?: ButtonLinkSkin;
11
+ readonly textLink?: TextLinkSkin;
12
+ readonly phoneLink?: PhoneLinkSkin;
13
+ readonly quoteButton?: QuoteButtonSkin;
14
+ readonly container?: ContainerSkin;
15
+ readonly sectionHeading?: SectionHeadingSkin;
16
+ }
17
+ //# sourceMappingURL=brandlab-skin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brandlab-skin.d.ts","sourceRoot":"","sources":["../../src/ui-kit/brandlab-skin.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAE/E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,kBAAkB,CAAA;CAC7C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=brandlab-skin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brandlab-skin.js","sourceRoot":"","sources":["../../src/ui-kit/brandlab-skin.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * A site's whole look for the ui-kit: one entry per shared component it renders (`src/site/skin.ts`, written with\n * `satisfies BrandlabSkin`). Each release adds the entries for its components. K2 checks that every rendered\n * component has an entry and that no reference-skin class ships.\n */\nimport type { ButtonLinkSkin, PhoneLinkSkin, TextLinkSkin } from './primitives/links.js'\nimport type { QuoteButtonSkin } from './primitives/quote-button.js'\nimport type { ContainerSkin, SectionHeadingSkin } from './primitives/layout.js'\n\nexport interface BrandlabSkin {\n readonly buttonLink?: ButtonLinkSkin\n readonly textLink?: TextLinkSkin\n readonly phoneLink?: PhoneLinkSkin\n readonly quoteButton?: QuoteButtonSkin\n readonly container?: ContainerSkin\n readonly sectionHeading?: SectionHeadingSkin\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { UiSocialKind } from './types.js';
2
+ export interface IconProps {
3
+ readonly className?: string | undefined;
4
+ }
5
+ export declare const UI_ICON_NAMES: readonly ["phone", "chevron", "arrow", "arrow-up", "menu", "close", "star", "check", "camera", "mail", "facebook", "instagram", "google", "linkedin", "youtube", "tiktok"];
6
+ export type UiIconName = (typeof UI_ICON_NAMES)[number];
7
+ /** One icon from the set by name. */
8
+ export declare function Icon({ name, className }: IconProps & {
9
+ name: UiIconName;
10
+ }): import("react/jsx-runtime").JSX.Element;
11
+ /** The icon for a social or Business Profile link. */
12
+ export declare function SocialIcon({ kind, className }: IconProps & {
13
+ kind: UiSocialKind;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../src/ui-kit/icons.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,eAAO,MAAM,aAAa,4KAiBhB,CAAA;AACV,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAA;AAwGvD,qCAAqC;AACrC,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,2CAOzE;AAED,sDAAsD;AACtD,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,2CAEjF"}
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const UI_ICON_NAMES = [
3
+ 'phone',
4
+ 'chevron',
5
+ 'arrow',
6
+ 'arrow-up',
7
+ 'menu',
8
+ 'close',
9
+ 'star',
10
+ 'check',
11
+ 'camera',
12
+ 'mail',
13
+ 'facebook',
14
+ 'instagram',
15
+ 'google',
16
+ 'linkedin',
17
+ 'youtube',
18
+ 'tiktok',
19
+ ];
20
+ function Svg({ name, viewBox = '0 0 24 24', className, children }) {
21
+ return (_jsx("svg", { "aria-hidden": "true", focusable: "false", viewBox: viewBox, className: className, "data-icon": name, children: children }));
22
+ }
23
+ const stroke = { fill: 'none', stroke: 'currentColor', strokeLinecap: 'round', strokeLinejoin: 'round' };
24
+ const GLYPHS = {
25
+ phone: {
26
+ body: (_jsx("path", { ...stroke, strokeWidth: "1.6", d: "M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106a1.125 1.125 0 0 0-1.173.417l-.97 1.293a1.125 1.125 0 0 1-1.21.38 12.035 12.035 0 0 1-7.143-7.143 1.125 1.125 0 0 1 .38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z" })),
27
+ },
28
+ chevron: { body: _jsx("path", { ...stroke, strokeWidth: "1.8", d: "m6 9 6 6 6-6" }) },
29
+ arrow: { body: _jsx("path", { ...stroke, strokeWidth: "1.6", d: "M13.5 4.5 21 12l-7.5 7.5M3 12h17.25" }) },
30
+ 'arrow-up': { body: _jsx("path", { ...stroke, strokeWidth: "1.8", d: "M12 20V4m0 0-7 7m7-7 7 7" }) },
31
+ menu: { body: _jsx("path", { ...stroke, strokeWidth: "1.7", d: "M4 7h16M4 12h16M4 17h16" }) },
32
+ close: { body: _jsx("path", { ...stroke, strokeWidth: "1.7", d: "M6 18 18 6M6 6l12 12" }) },
33
+ star: {
34
+ viewBox: '0 0 20 20',
35
+ body: _jsx("path", { fill: "currentColor", d: "M10 1.6 12.6 7l5.9.7-4.4 4 1.2 5.8L10 14.6l-5.3 2.9L5.9 11.7l-4.4-4L7.4 7 10 1.6Z" }),
36
+ },
37
+ check: { body: _jsx("path", { ...stroke, strokeWidth: "2", d: "m4.5 12.75 6 6 9-13.5" }) },
38
+ camera: {
39
+ body: (_jsxs(_Fragment, { children: [_jsx("path", { ...stroke, strokeWidth: "1.6", d: "M3 8.25A2.25 2.25 0 0 1 5.25 6h1.372c.516 0 .966-.351 1.091-.852l.38-1.52A1.125 1.125 0 0 1 9.184 2.7h5.632c.516 0 .966.351 1.091.852l.38 1.52c.125.5.575.852 1.091.852h1.372A2.25 2.25 0 0 1 21 8.25v9.75a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 18V8.25Z" }), _jsx("circle", { ...stroke, strokeWidth: "1.6", cx: "12", cy: "12.75", r: "3.75" })] })),
40
+ },
41
+ mail: {
42
+ body: (_jsx("path", { ...stroke, strokeWidth: "1.6", d: "M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0-8.69 5.6a2.25 2.25 0 0 1-2.12 0l-8.69-5.6" })),
43
+ },
44
+ facebook: {
45
+ body: (_jsx("path", { fill: "currentColor", d: "M9 8H6v4h3v12h5V12h3.642L18 8h-4V6.333C14 5.378 14.192 5 15.115 5H18V0h-3.808C10.596 0 9 1.583 9 4.615V8Z" })),
46
+ },
47
+ instagram: {
48
+ body: (_jsxs(_Fragment, { children: [_jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "5", fill: "none", stroke: "currentColor", strokeWidth: "2" }), _jsx("circle", { cx: "12", cy: "12", r: "4.2", fill: "none", stroke: "currentColor", strokeWidth: "2" }), _jsx("circle", { cx: "17.4", cy: "6.6", r: "1.3", fill: "currentColor" })] })),
49
+ },
50
+ google: {
51
+ body: (_jsx("path", { fill: "currentColor", d: "M21.6 12.23c0-.68-.06-1.33-.17-1.96H12v3.7h5.39a4.6 4.6 0 0 1-2 3.02v2.5h3.24c1.9-1.75 2.97-4.32 2.97-7.26ZM12 22c2.7 0 4.96-.9 6.62-2.43l-3.24-2.5c-.9.6-2.04.95-3.38.95-2.6 0-4.8-1.75-5.59-4.11H3.07v2.59A10 10 0 0 0 12 22Zm-5.59-8.09A6 6 0 0 1 6.1 12c0-.66.11-1.31.31-1.91V7.5H3.07A10 10 0 0 0 2 12c0 1.61.39 3.14 1.07 4.5l3.34-2.59ZM12 5.98c1.47 0 2.79.5 3.83 1.5l2.87-2.87A9.6 9.6 0 0 0 12 2a10 10 0 0 0-8.93 5.5l3.34 2.59C7.2 7.73 9.4 5.98 12 5.98Z" })),
52
+ },
53
+ linkedin: {
54
+ body: (_jsx("path", { fill: "currentColor", d: "M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM3 9.75h4v11.5H3V9.75Zm6.5 0h3.83v1.57h.06c.53-1 1.84-2.07 3.79-2.07 4.05 0 4.8 2.67 4.8 6.13v5.87h-4v-5.2c0-1.24-.02-2.84-1.73-2.84-1.73 0-2 1.35-2 2.75v5.29h-4V9.75Z" })),
55
+ },
56
+ youtube: {
57
+ body: (_jsx("path", { fill: "currentColor", d: "M23 7.2a3 3 0 0 0-2.1-2.1C19 4.6 12 4.6 12 4.6s-7 0-8.9.5A3 3 0 0 0 1 7.2 31 31 0 0 0 .5 12a31 31 0 0 0 .5 4.8 3 3 0 0 0 2.1 2.1c1.9.5 8.9.5 8.9.5s7 0 8.9-.5a3 3 0 0 0 2.1-2.1 31 31 0 0 0 .5-4.8 31 31 0 0 0-.5-4.8ZM9.75 15.02V8.98L15.5 12l-5.75 3.02Z" })),
58
+ },
59
+ tiktok: {
60
+ body: (_jsx("path", { fill: "currentColor", d: "M16.6 2h-3.4v13.4a2.9 2.9 0 1 1-2.9-2.9c.3 0 .6 0 .9.1V9.1a6.3 6.3 0 1 0 5.4 6.3V8.6a8.1 8.1 0 0 0 4.4 1.3V6.5a4.5 4.5 0 0 1-4.4-4.5Z" })),
61
+ },
62
+ };
63
+ /** One icon from the set by name. */
64
+ export function Icon({ name, className }) {
65
+ const glyph = GLYPHS[name];
66
+ return (_jsx(Svg, { name: name, viewBox: glyph.viewBox, className: className, children: glyph.body }));
67
+ }
68
+ /** The icon for a social or Business Profile link. */
69
+ export function SocialIcon({ kind, className }) {
70
+ return _jsx(Icon, { name: kind, className: className });
71
+ }
72
+ //# sourceMappingURL=icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icons.js","sourceRoot":"","sources":["../../src/ui-kit/icons.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * The ui-kit's inline SVG icon set (no icon font). Every icon is decorative (`aria-hidden`, not focusable): the\n * control around it carries the accessible name. Stroke and fill use `currentColor`, so a skin colours them through\n * its classes or tokens. `data-icon` names the glyph for skins and tests.\n */\nimport type { ReactNode } from 'react'\nimport type { UiSocialKind } from './types.js'\n\nexport interface IconProps {\n readonly className?: string | undefined\n}\n\nexport const UI_ICON_NAMES = [\n 'phone',\n 'chevron',\n 'arrow',\n 'arrow-up',\n 'menu',\n 'close',\n 'star',\n 'check',\n 'camera',\n 'mail',\n 'facebook',\n 'instagram',\n 'google',\n 'linkedin',\n 'youtube',\n 'tiktok',\n] as const\nexport type UiIconName = (typeof UI_ICON_NAMES)[number]\n\nfunction Svg({ name, viewBox = '0 0 24 24', className, children }: IconProps & { name: UiIconName; viewBox?: string; children: ReactNode }) {\n return (\n <svg aria-hidden=\"true\" focusable=\"false\" viewBox={viewBox} className={className} data-icon={name}>\n {children}\n </svg>\n )\n}\n\nconst stroke = { fill: 'none', stroke: 'currentColor', strokeLinecap: 'round', strokeLinejoin: 'round' } as const\n\nconst GLYPHS: Record<UiIconName, { viewBox?: string; body: ReactNode }> = {\n phone: {\n body: (\n <path\n {...stroke}\n strokeWidth=\"1.6\"\n d=\"M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106a1.125 1.125 0 0 0-1.173.417l-.97 1.293a1.125 1.125 0 0 1-1.21.38 12.035 12.035 0 0 1-7.143-7.143 1.125 1.125 0 0 1 .38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z\"\n />\n ),\n },\n chevron: { body: <path {...stroke} strokeWidth=\"1.8\" d=\"m6 9 6 6 6-6\" /> },\n arrow: { body: <path {...stroke} strokeWidth=\"1.6\" d=\"M13.5 4.5 21 12l-7.5 7.5M3 12h17.25\" /> },\n 'arrow-up': { body: <path {...stroke} strokeWidth=\"1.8\" d=\"M12 20V4m0 0-7 7m7-7 7 7\" /> },\n menu: { body: <path {...stroke} strokeWidth=\"1.7\" d=\"M4 7h16M4 12h16M4 17h16\" /> },\n close: { body: <path {...stroke} strokeWidth=\"1.7\" d=\"M6 18 18 6M6 6l12 12\" /> },\n star: {\n viewBox: '0 0 20 20',\n body: <path fill=\"currentColor\" d=\"M10 1.6 12.6 7l5.9.7-4.4 4 1.2 5.8L10 14.6l-5.3 2.9L5.9 11.7l-4.4-4L7.4 7 10 1.6Z\" />,\n },\n check: { body: <path {...stroke} strokeWidth=\"2\" d=\"m4.5 12.75 6 6 9-13.5\" /> },\n camera: {\n body: (\n <>\n <path\n {...stroke}\n strokeWidth=\"1.6\"\n d=\"M3 8.25A2.25 2.25 0 0 1 5.25 6h1.372c.516 0 .966-.351 1.091-.852l.38-1.52A1.125 1.125 0 0 1 9.184 2.7h5.632c.516 0 .966.351 1.091.852l.38 1.52c.125.5.575.852 1.091.852h1.372A2.25 2.25 0 0 1 21 8.25v9.75a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 18V8.25Z\"\n />\n <circle {...stroke} strokeWidth=\"1.6\" cx=\"12\" cy=\"12.75\" r=\"3.75\" />\n </>\n ),\n },\n mail: {\n body: (\n <path\n {...stroke}\n strokeWidth=\"1.6\"\n d=\"M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0-8.69 5.6a2.25 2.25 0 0 1-2.12 0l-8.69-5.6\"\n />\n ),\n },\n facebook: {\n body: (\n <path\n fill=\"currentColor\"\n d=\"M9 8H6v4h3v12h5V12h3.642L18 8h-4V6.333C14 5.378 14.192 5 15.115 5H18V0h-3.808C10.596 0 9 1.583 9 4.615V8Z\"\n />\n ),\n },\n instagram: {\n body: (\n <>\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" />\n <circle cx=\"12\" cy=\"12\" r=\"4.2\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" />\n <circle cx=\"17.4\" cy=\"6.6\" r=\"1.3\" fill=\"currentColor\" />\n </>\n ),\n },\n google: {\n body: (\n <path\n fill=\"currentColor\"\n d=\"M21.6 12.23c0-.68-.06-1.33-.17-1.96H12v3.7h5.39a4.6 4.6 0 0 1-2 3.02v2.5h3.24c1.9-1.75 2.97-4.32 2.97-7.26ZM12 22c2.7 0 4.96-.9 6.62-2.43l-3.24-2.5c-.9.6-2.04.95-3.38.95-2.6 0-4.8-1.75-5.59-4.11H3.07v2.59A10 10 0 0 0 12 22Zm-5.59-8.09A6 6 0 0 1 6.1 12c0-.66.11-1.31.31-1.91V7.5H3.07A10 10 0 0 0 2 12c0 1.61.39 3.14 1.07 4.5l3.34-2.59ZM12 5.98c1.47 0 2.79.5 3.83 1.5l2.87-2.87A9.6 9.6 0 0 0 12 2a10 10 0 0 0-8.93 5.5l3.34 2.59C7.2 7.73 9.4 5.98 12 5.98Z\"\n />\n ),\n },\n linkedin: {\n body: (\n <path\n fill=\"currentColor\"\n d=\"M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM3 9.75h4v11.5H3V9.75Zm6.5 0h3.83v1.57h.06c.53-1 1.84-2.07 3.79-2.07 4.05 0 4.8 2.67 4.8 6.13v5.87h-4v-5.2c0-1.24-.02-2.84-1.73-2.84-1.73 0-2 1.35-2 2.75v5.29h-4V9.75Z\"\n />\n ),\n },\n youtube: {\n body: (\n <path\n fill=\"currentColor\"\n d=\"M23 7.2a3 3 0 0 0-2.1-2.1C19 4.6 12 4.6 12 4.6s-7 0-8.9.5A3 3 0 0 0 1 7.2 31 31 0 0 0 .5 12a31 31 0 0 0 .5 4.8 3 3 0 0 0 2.1 2.1c1.9.5 8.9.5 8.9.5s7 0 8.9-.5a3 3 0 0 0 2.1-2.1 31 31 0 0 0 .5-4.8 31 31 0 0 0-.5-4.8ZM9.75 15.02V8.98L15.5 12l-5.75 3.02Z\"\n />\n ),\n },\n tiktok: {\n body: (\n <path\n fill=\"currentColor\"\n d=\"M16.6 2h-3.4v13.4a2.9 2.9 0 1 1-2.9-2.9c.3 0 .6 0 .9.1V9.1a6.3 6.3 0 1 0 5.4 6.3V8.6a8.1 8.1 0 0 0 4.4 1.3V6.5a4.5 4.5 0 0 1-4.4-4.5Z\"\n />\n ),\n },\n}\n\n/** One icon from the set by name. */\nexport function Icon({ name, className }: IconProps & { name: UiIconName }) {\n const glyph = GLYPHS[name]\n return (\n <Svg name={name} viewBox={glyph.viewBox} className={className}>\n {glyph.body}\n </Svg>\n )\n}\n\n/** The icon for a social or Business Profile link. */\nexport function SocialIcon({ kind, className }: IconProps & { kind: UiSocialKind }) {\n return <Icon name={kind} className={className} />\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export { BUTTON_VARIANTS, ButtonLink, PhoneLink, TextLink, type ButtonLinkProps, type ButtonLinkSkin, type ButtonLinkSlot, type ButtonVariant, type PhoneLinkProps, type PhoneLinkSkin, type PhoneLinkSlot, type TextLinkProps, type TextLinkSkin, type TextLinkSlot, } from './links.js';
2
+ export { CONTAINER_WIDTHS, Container, RatingStars, SECTION_TONES, Section, SectionHeading, VisuallyHidden, type ContainerSkin, type ContainerWidth, type SectionHeadingProps, type SectionHeadingSkin, type SectionHeadingSlot, type SectionProps, type SectionSlot, type SectionTone, } from './layout.js';
3
+ export { QuoteButton, type QuoteButtonProps, type QuoteButtonSkin, type QuoteButtonSlot } from './quote-button.js';
4
+ export { Icon, SocialIcon, UI_ICON_NAMES, type IconProps, type UiIconName } from '../icons.js';
5
+ export { UI_SOCIAL_KINDS, type UiLink, type UiPhone, type UiSocial, type UiSocialKind } from '../types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,SAAS,EACT,QAAQ,EACR,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,aAAa,EACb,OAAO,EACP,cAAc,EACd,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAClH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,eAAe,EAAE,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,6 @@
1
+ export { BUTTON_VARIANTS, ButtonLink, PhoneLink, TextLink, } from './links.js';
2
+ export { CONTAINER_WIDTHS, Container, RatingStars, SECTION_TONES, Section, SectionHeading, VisuallyHidden, } from './layout.js';
3
+ export { QuoteButton } from './quote-button.js';
4
+ export { Icon, SocialIcon, UI_ICON_NAMES } from '../icons.js';
5
+ export { UI_SOCIAL_KINDS } from '../types.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,UAAU,EACV,SAAS,EACT,QAAQ,GAWT,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,aAAa,EACb,OAAO,EACP,cAAc,EACd,cAAc,GASf,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,WAAW,EAAqE,MAAM,mBAAmB,CAAA;AAClH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAmC,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,eAAe,EAA+D,MAAM,aAAa,CAAA","sourcesContent":["export {\n BUTTON_VARIANTS,\n ButtonLink,\n PhoneLink,\n TextLink,\n type ButtonLinkProps,\n type ButtonLinkSkin,\n type ButtonLinkSlot,\n type ButtonVariant,\n type PhoneLinkProps,\n type PhoneLinkSkin,\n type PhoneLinkSlot,\n type TextLinkProps,\n type TextLinkSkin,\n type TextLinkSlot,\n} from './links.js'\nexport {\n CONTAINER_WIDTHS,\n Container,\n RatingStars,\n SECTION_TONES,\n Section,\n SectionHeading,\n VisuallyHidden,\n type ContainerSkin,\n type ContainerWidth,\n type SectionHeadingProps,\n type SectionHeadingSkin,\n type SectionHeadingSlot,\n type SectionProps,\n type SectionSlot,\n type SectionTone,\n} from './layout.js'\nexport { QuoteButton, type QuoteButtonProps, type QuoteButtonSkin, type QuoteButtonSlot } from './quote-button.js'\nexport { Icon, SocialIcon, UI_ICON_NAMES, type IconProps, type UiIconName } from '../icons.js'\nexport { UI_SOCIAL_KINDS, type UiLink, type UiPhone, type UiSocial, type UiSocialKind } from '../types.js'\n"]}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Layout primitives: `Container`, `Section`, `SectionHeading`, `Eyebrow`, `RatingStars` and `VisuallyHidden`.
3
+ * Server components. Heading levels come from the page outline (one H1 in the hero or page header, an H2 per
4
+ * section), never from a look.
5
+ */
6
+ import type { ReactNode } from 'react';
7
+ import type { PhoneCallPlacement } from '../../analytics.js';
8
+ import { type SkinEntry } from '../skin.js';
9
+ export declare const CONTAINER_WIDTHS: readonly ["default", "narrow", "wide"];
10
+ export type ContainerWidth = (typeof CONTAINER_WIDTHS)[number];
11
+ export type ContainerSkin = SkinEntry<'root', never, {
12
+ width?: ContainerWidth;
13
+ }>;
14
+ /** One container width shared by the header, the page and the footer (`--bl-container`, `--bl-gutter`). */
15
+ export declare function Container({ children, layout, classes }: ContainerSkin & {
16
+ readonly children: ReactNode;
17
+ }): import("react/jsx-runtime").JSX.Element;
18
+ /** The four section tones `SectionSequence` assigns: two alternating grounds, one optional band, one photo ground. */
19
+ export declare const SECTION_TONES: readonly ["canvas", "surface", "band", "photo"];
20
+ export type SectionTone = (typeof SECTION_TONES)[number];
21
+ export type SectionSlot = 'root' | 'inner';
22
+ export interface SectionProps {
23
+ /** The matrix id, rendered as `data-bl-section` (for example `reviews`, `cta`, `faqs`). */
24
+ readonly id: string;
25
+ /** The id of the section's H2. A section is a `<section>` labelled by its heading. */
26
+ readonly labelledBy: string;
27
+ readonly tone?: SectionTone | undefined;
28
+ readonly variant?: string | undefined;
29
+ readonly classes?: Readonly<Partial<Record<SectionSlot, string>>> | undefined;
30
+ /** Signature art, rendered `aria-hidden`. */
31
+ readonly decoration?: ReactNode;
32
+ /**
33
+ * Extra layout hooks on the root, for example `{ 'data-columns': '3' }`. They cannot override `data-bl-section`,
34
+ * `data-variant`, `data-tone` or `data-phone-location`.
35
+ */
36
+ readonly hooks?: Readonly<Record<`data-${string}`, string | undefined>>;
37
+ /** The placement a `tel:` link inside the section reports when it has none of its own. */
38
+ readonly phonePlacement?: PhoneCallPlacement | undefined;
39
+ readonly children: ReactNode;
40
+ }
41
+ /**
42
+ * The root every section component renders: `<section data-bl-section aria-labelledby>`, its tone and variant, an
43
+ * optional `aria-hidden` decoration and the inner container slot.
44
+ */
45
+ export declare function Section({ id, labelledBy, tone, variant, classes, decoration, hooks, phonePlacement, children }: SectionProps): import("react/jsx-runtime").JSX.Element;
46
+ export type SectionHeadingSlot = 'head' | 'eyebrow' | 'heading' | 'lead';
47
+ export type SectionHeadingSkin = SkinEntry<SectionHeadingSlot, never, {
48
+ align?: 'start' | 'center';
49
+ }>;
50
+ export interface SectionHeadingProps extends SectionHeadingSkin {
51
+ /** The H2's id, which the section's `aria-labelledby` points at. */
52
+ readonly id: string;
53
+ readonly title: ReactNode;
54
+ /** A short label above the H2 (at least 13 px; never the only label for the section). */
55
+ readonly eyebrow?: ReactNode;
56
+ readonly lead?: ReactNode;
57
+ }
58
+ /** A section's heading block: an optional eyebrow, the H2 and an optional lead paragraph. */
59
+ export declare function SectionHeading({ id, title, eyebrow, lead, layout, classes }: SectionHeadingProps): import("react/jsx-runtime").JSX.Element;
60
+ /**
61
+ * Five stars with one text alternative ("Rated 4.9 out of 5"). The stars use `--bl-color-star`; whole stars only,
62
+ * rounded to the nearest. The rating always comes from the feed, never typed by hand (T4).
63
+ */
64
+ export declare function RatingStars({ rating, label, className }: {
65
+ readonly rating: number;
66
+ readonly label?: string;
67
+ readonly className?: string;
68
+ }): import("react/jsx-runtime").JSX.Element;
69
+ /** Text for assistive technology only. */
70
+ export declare function VisuallyHidden({ children }: {
71
+ readonly children: ReactNode;
72
+ }): import("react/jsx-runtime").JSX.Element;
73
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/layout.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from '../icons.js';
3
+ import { slot } from '../skin.js';
4
+ export const CONTAINER_WIDTHS = ['default', 'narrow', 'wide'];
5
+ /** One container width shared by the header, the page and the footer (`--bl-container`, `--bl-gutter`). */
6
+ export function Container({ children, layout, classes }) {
7
+ return (_jsx("div", { "data-bl": "container", "data-width": layout?.width ?? 'default', className: classes?.root, children: children }));
8
+ }
9
+ /** The four section tones `SectionSequence` assigns: two alternating grounds, one optional band, one photo ground. */
10
+ export const SECTION_TONES = ['canvas', 'surface', 'band', 'photo'];
11
+ /**
12
+ * The root every section component renders: `<section data-bl-section aria-labelledby>`, its tone and variant, an
13
+ * optional `aria-hidden` decoration and the inner container slot.
14
+ */
15
+ export function Section({ id, labelledBy, tone, variant, classes, decoration, hooks, phonePlacement, children }) {
16
+ return (_jsxs("section", { ...hooks, "data-bl-section": id, "data-variant": variant, "data-tone": tone, "data-phone-location": phonePlacement, "aria-labelledby": labelledBy, className: classes?.root, children: [decoration ? (_jsx("div", { "data-slot": "decoration", "aria-hidden": "true", children: decoration })) : null, _jsx("div", { ...slot(classes, 'inner'), children: children })] }));
17
+ }
18
+ /** A section's heading block: an optional eyebrow, the H2 and an optional lead paragraph. */
19
+ export function SectionHeading({ id, title, eyebrow, lead, layout, classes }) {
20
+ return (_jsxs("div", { ...slot(classes, 'head'), "data-align": layout?.align ?? 'start', children: [eyebrow ? _jsx("p", { ...slot(classes, 'eyebrow'), children: eyebrow }) : null, _jsx("h2", { id: id, ...slot(classes, 'heading'), children: title }), lead ? _jsx("p", { ...slot(classes, 'lead'), children: lead }) : null] }));
21
+ }
22
+ /**
23
+ * Five stars with one text alternative ("Rated 4.9 out of 5"). The stars use `--bl-color-star`; whole stars only,
24
+ * rounded to the nearest. The rating always comes from the feed, never typed by hand (T4).
25
+ */
26
+ export function RatingStars({ rating, label, className }) {
27
+ const filled = Math.max(0, Math.min(5, Math.round(rating)));
28
+ return (_jsx("span", { "data-bl": "stars", role: "img", "aria-label": label ?? `Rated ${rating.toFixed(1)} out of 5`, className: className, children: Array.from({ length: 5 }, (_, index) => (_jsx("span", { "data-slot": "star", "data-state": index < filled ? 'on' : 'off', children: _jsx(Icon, { name: "star" }) }, index))) }));
29
+ }
30
+ /** Text for assistive technology only. */
31
+ export function VisuallyHidden({ children }) {
32
+ return _jsx("span", { "data-bl-visually-hidden": "", children: children });
33
+ }
34
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/layout.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Layout primitives: `Container`, `Section`, `SectionHeading`, `Eyebrow`, `RatingStars` and `VisuallyHidden`.\n * Server components. Heading levels come from the page outline (one H1 in the hero or page header, an H2 per\n * section), never from a look.\n */\nimport type { ReactNode } from 'react'\nimport type { PhoneCallPlacement } from '../../analytics.js'\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\n\nexport const CONTAINER_WIDTHS = ['default', 'narrow', 'wide'] as const\nexport type ContainerWidth = (typeof CONTAINER_WIDTHS)[number]\nexport type ContainerSkin = SkinEntry<'root', never, { width?: ContainerWidth }>\n\n/** One container width shared by the header, the page and the footer (`--bl-container`, `--bl-gutter`). */\nexport function Container({ children, layout, classes }: ContainerSkin & { readonly children: ReactNode }) {\n return (\n <div data-bl=\"container\" data-width={layout?.width ?? 'default'} className={classes?.root}>\n {children}\n </div>\n )\n}\n\n/** The four section tones `SectionSequence` assigns: two alternating grounds, one optional band, one photo ground. */\nexport const SECTION_TONES = ['canvas', 'surface', 'band', 'photo'] as const\nexport type SectionTone = (typeof SECTION_TONES)[number]\n\nexport type SectionSlot = 'root' | 'inner'\n\nexport interface SectionProps {\n /** The matrix id, rendered as `data-bl-section` (for example `reviews`, `cta`, `faqs`). */\n readonly id: string\n /** The id of the section's H2. A section is a `<section>` labelled by its heading. */\n readonly labelledBy: string\n readonly tone?: SectionTone | undefined\n readonly variant?: string | undefined\n readonly classes?: Readonly<Partial<Record<SectionSlot, string>>> | undefined\n /** Signature art, rendered `aria-hidden`. */\n readonly decoration?: ReactNode\n /**\n * Extra layout hooks on the root, for example `{ 'data-columns': '3' }`. They cannot override `data-bl-section`,\n * `data-variant`, `data-tone` or `data-phone-location`.\n */\n readonly hooks?: Readonly<Record<`data-${string}`, string | undefined>>\n /** The placement a `tel:` link inside the section reports when it has none of its own. */\n readonly phonePlacement?: PhoneCallPlacement | undefined\n readonly children: ReactNode\n}\n\n/**\n * The root every section component renders: `<section data-bl-section aria-labelledby>`, its tone and variant, an\n * optional `aria-hidden` decoration and the inner container slot.\n */\nexport function Section({ id, labelledBy, tone, variant, classes, decoration, hooks, phonePlacement, children }: SectionProps) {\n return (\n <section\n {...hooks}\n data-bl-section={id}\n data-variant={variant}\n data-tone={tone}\n data-phone-location={phonePlacement}\n aria-labelledby={labelledBy}\n className={classes?.root}\n >\n {decoration ? (\n <div data-slot=\"decoration\" aria-hidden=\"true\">\n {decoration}\n </div>\n ) : null}\n <div {...slot(classes, 'inner')}>{children}</div>\n </section>\n )\n}\n\nexport type SectionHeadingSlot = 'head' | 'eyebrow' | 'heading' | 'lead'\nexport type SectionHeadingSkin = SkinEntry<SectionHeadingSlot, never, { align?: 'start' | 'center' }>\n\nexport interface SectionHeadingProps extends SectionHeadingSkin {\n /** The H2's id, which the section's `aria-labelledby` points at. */\n readonly id: string\n readonly title: ReactNode\n /** A short label above the H2 (at least 13 px; never the only label for the section). */\n readonly eyebrow?: ReactNode\n readonly lead?: ReactNode\n}\n\n/** A section's heading block: an optional eyebrow, the H2 and an optional lead paragraph. */\nexport function SectionHeading({ id, title, eyebrow, lead, layout, classes }: SectionHeadingProps) {\n return (\n <div {...slot(classes, 'head')} data-align={layout?.align ?? 'start'}>\n {eyebrow ? <p {...slot(classes, 'eyebrow')}>{eyebrow}</p> : null}\n <h2 id={id} {...slot(classes, 'heading')}>\n {title}\n </h2>\n {lead ? <p {...slot(classes, 'lead')}>{lead}</p> : null}\n </div>\n )\n}\n\n/**\n * Five stars with one text alternative (\"Rated 4.9 out of 5\"). The stars use `--bl-color-star`; whole stars only,\n * rounded to the nearest. The rating always comes from the feed, never typed by hand (T4).\n */\nexport function RatingStars({ rating, label, className }: { readonly rating: number; readonly label?: string; readonly className?: string }) {\n const filled = Math.max(0, Math.min(5, Math.round(rating)))\n return (\n <span data-bl=\"stars\" role=\"img\" aria-label={label ?? `Rated ${rating.toFixed(1)} out of 5`} className={className}>\n {Array.from({ length: 5 }, (_, index) => (\n <span key={index} data-slot=\"star\" data-state={index < filled ? 'on' : 'off'}>\n <Icon name=\"star\" />\n </span>\n ))}\n </span>\n )\n}\n\n/** Text for assistive technology only. */\nexport function VisuallyHidden({ children }: { readonly children: ReactNode }) {\n return <span data-bl-visually-hidden=\"\">{children}</span>\n}\n"]}
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PhoneCallPlacement } from '../../analytics.js';
3
+ import { type UiIconName } from '../icons.js';
4
+ import { type SkinEntry } from '../skin.js';
5
+ import type { UiPhone } from '../types.js';
6
+ export type ButtonLinkSlot = 'root' | 'label' | 'icon';
7
+ export declare const BUTTON_VARIANTS: readonly ["primary", "secondary", "quiet"];
8
+ export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
9
+ export type ButtonLinkSkin = SkinEntry<ButtonLinkSlot, ButtonVariant, {
10
+ size?: 'regular' | 'large';
11
+ }>;
12
+ export interface ButtonLinkProps extends ButtonLinkSkin {
13
+ readonly href: string;
14
+ readonly children: ReactNode;
15
+ /** An icon after the label (`arrow`) or before it (`phone`). */
16
+ readonly icon?: UiIconName;
17
+ readonly iconPosition?: 'start' | 'end';
18
+ /** Force external handling; by default `http(s)://` links are external. */
19
+ readonly external?: boolean;
20
+ }
21
+ /** A link styled as a button. A primary action is at least 44 px tall (structure.css). */
22
+ export declare function ButtonLink({ href, children, icon, iconPosition, external, variant, layout, classes }: ButtonLinkProps): import("react/jsx-runtime").JSX.Element;
23
+ export type TextLinkSlot = 'root';
24
+ export type TextLinkSkin = SkinEntry<TextLinkSlot, 'inline' | 'standalone'>;
25
+ export interface TextLinkProps extends TextLinkSkin {
26
+ readonly href: string;
27
+ readonly children: ReactNode;
28
+ readonly external?: boolean;
29
+ }
30
+ /** An inline or standalone text link. */
31
+ export declare function TextLink({ href, children, external, variant, classes }: TextLinkProps): import("react/jsx-runtime").JSX.Element;
32
+ export type PhoneLinkSlot = 'root' | 'icon' | 'label' | 'number';
33
+ export type PhoneLinkSkin = SkinEntry<PhoneLinkSlot, 'text' | 'button'>;
34
+ export interface PhoneLinkProps extends PhoneLinkSkin {
35
+ readonly phone: UiPhone;
36
+ /** Where the link sits, for `phone_link_click`. Required: a section sets its own. */
37
+ readonly placement: PhoneCallPlacement;
38
+ /** Optional words before the number, for example "Call". The accessible name is always the visible text. */
39
+ readonly label?: string;
40
+ /** The phone icon on the left (header and hero). */
41
+ readonly showIcon?: boolean;
42
+ }
43
+ /**
44
+ * A `tel:` link. Its accessible name is its visible text, which always includes the number (WCAG 2.5.3). Use it for
45
+ * every call link: a literal `tel:` anywhere else fails lint (K1) and C10 checks the href against the evidence file.
46
+ */
47
+ export declare function PhoneLink({ phone, placement, label, showIcon, variant, classes }: PhoneLinkProps): import("react/jsx-runtime").JSX.Element;
48
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/links.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AACnD,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAwB1C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACtD,eAAO,MAAM,eAAe,4CAA6C,CAAA;AACzE,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAC5D,MAAM,MAAM,cAAc,GAAG,SAAS,CAAC,cAAc,EAAE,aAAa,EAAE;IAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;CAAE,CAAC,CAAA;AAErG,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,GAAG,KAAK,CAAA;IACvC,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,0FAA0F;AAC1F,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAoB,EAAE,QAAQ,EAAE,OAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,eAAe,2CAazI;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAA;AACjC,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,EAAE,QAAQ,GAAG,YAAY,CAAC,CAAA;AAE3E,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAkB,EAAE,OAAO,EAAE,EAAE,aAAa,2CAMhG;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAChE,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAA;AAEvE,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,qFAAqF;IACrF,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAA;IACtC,4GAA4G;IAC5G,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAgB,EAAE,OAAgB,EAAE,OAAO,EAAE,EAAE,cAAc,2CAgBjH"}
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Link primitives: `ButtonLink`, `TextLink` and `PhoneLink`. Server components; every link is a real `<a href>`.
4
+ * External links open in a new tab with `rel="noopener noreferrer"`. `PhoneLink` requires a typed placement, which the
5
+ * shared instrumentation reads to send one `phone_link_click` per tap; nothing here emits events itself.
6
+ */
7
+ import Link from 'next/link';
8
+ import { Icon } from '../icons.js';
9
+ import { slot } from '../skin.js';
10
+ function isExternal(href) {
11
+ return /^https?:\/\//i.test(href);
12
+ }
13
+ function Anchor({ href, external, hooks, children }) {
14
+ if (external ?? isExternal(href)) {
15
+ return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", ...hooks, children: children }));
16
+ }
17
+ return (_jsx(Link, { href: href, ...hooks, children: children }));
18
+ }
19
+ export const BUTTON_VARIANTS = ['primary', 'secondary', 'quiet'];
20
+ /** A link styled as a button. A primary action is at least 44 px tall (structure.css). */
21
+ export function ButtonLink({ href, children, icon, iconPosition = 'end', external, variant = 'primary', layout, classes }) {
22
+ const iconNode = icon ? _jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: icon }) }) : null;
23
+ return (_jsxs(Anchor, { href: href, external: external, hooks: { 'data-bl': 'button', 'data-variant': variant, 'data-size': layout?.size ?? 'regular', className: classes?.root }, children: [iconPosition === 'start' ? iconNode : null, _jsx("span", { ...slot(classes, 'label'), children: children }), iconPosition === 'end' ? iconNode : null] }));
24
+ }
25
+ /** An inline or standalone text link. */
26
+ export function TextLink({ href, children, external, variant = 'inline', classes }) {
27
+ return (_jsx(Anchor, { href: href, external: external, hooks: { 'data-bl': 'text-link', 'data-variant': variant, className: classes?.root }, children: children }));
28
+ }
29
+ /**
30
+ * A `tel:` link. Its accessible name is its visible text, which always includes the number (WCAG 2.5.3). Use it for
31
+ * every call link: a literal `tel:` anywhere else fails lint (K1) and C10 checks the href against the evidence file.
32
+ */
33
+ export function PhoneLink({ phone, placement, label, showIcon = false, variant = 'text', classes }) {
34
+ return (_jsxs("a", { href: phone.telHref, "data-bl": "phone-link", "data-variant": variant, "data-phone-location": placement, className: classes?.root, children: [showIcon ? (_jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "phone" }) })) : null, label ? (_jsxs(_Fragment, { children: [_jsx("span", { ...slot(classes, 'label'), children: label }), ' '] })) : null, _jsx("span", { ...slot(classes, 'number'), children: phone.display })] }));
35
+ }
36
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/links.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Link primitives: `ButtonLink`, `TextLink` and `PhoneLink`. Server components; every link is a real `<a href>`.\n * External links open in a new tab with `rel=\"noopener noreferrer\"`. `PhoneLink` requires a typed placement, which the\n * shared instrumentation reads to send one `phone_link_click` per tap; nothing here emits events itself.\n */\nimport Link from 'next/link'\nimport type { ReactNode } from 'react'\nimport type { PhoneCallPlacement } from '../../analytics.js'\nimport { Icon, type UiIconName } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiPhone } from '../types.js'\n\nfunction isExternal(href: string) {\n return /^https?:\\/\\//i.test(href)\n}\n\n/** Data hooks and the skin class for an anchor's root. */\ntype AnchorHooks = { readonly className: string | undefined } & Readonly<Record<`data-${string}`, string | undefined>>\n\nfunction Anchor({ href, external, hooks, children }: { href: string; external?: boolean | undefined; hooks: AnchorHooks; children: ReactNode }) {\n if (external ?? isExternal(href)) {\n return (\n <a href={href} target=\"_blank\" rel=\"noopener noreferrer\" {...hooks}>\n {children}\n </a>\n )\n }\n return (\n <Link href={href} {...hooks}>\n {children}\n </Link>\n )\n}\n\nexport type ButtonLinkSlot = 'root' | 'label' | 'icon'\nexport const BUTTON_VARIANTS = ['primary', 'secondary', 'quiet'] as const\nexport type ButtonVariant = (typeof BUTTON_VARIANTS)[number]\nexport type ButtonLinkSkin = SkinEntry<ButtonLinkSlot, ButtonVariant, { size?: 'regular' | 'large' }>\n\nexport interface ButtonLinkProps extends ButtonLinkSkin {\n readonly href: string\n readonly children: ReactNode\n /** An icon after the label (`arrow`) or before it (`phone`). */\n readonly icon?: UiIconName\n readonly iconPosition?: 'start' | 'end'\n /** Force external handling; by default `http(s)://` links are external. */\n readonly external?: boolean\n}\n\n/** A link styled as a button. A primary action is at least 44 px tall (structure.css). */\nexport function ButtonLink({ href, children, icon, iconPosition = 'end', external, variant = 'primary', layout, classes }: ButtonLinkProps) {\n const iconNode = icon ? <span {...slot(classes, 'icon')}><Icon name={icon} /></span> : null\n return (\n <Anchor\n href={href}\n external={external}\n hooks={{ 'data-bl': 'button', 'data-variant': variant, 'data-size': layout?.size ?? 'regular', className: classes?.root }}\n >\n {iconPosition === 'start' ? iconNode : null}\n <span {...slot(classes, 'label')}>{children}</span>\n {iconPosition === 'end' ? iconNode : null}\n </Anchor>\n )\n}\n\nexport type TextLinkSlot = 'root'\nexport type TextLinkSkin = SkinEntry<TextLinkSlot, 'inline' | 'standalone'>\n\nexport interface TextLinkProps extends TextLinkSkin {\n readonly href: string\n readonly children: ReactNode\n readonly external?: boolean\n}\n\n/** An inline or standalone text link. */\nexport function TextLink({ href, children, external, variant = 'inline', classes }: TextLinkProps) {\n return (\n <Anchor href={href} external={external} hooks={{ 'data-bl': 'text-link', 'data-variant': variant, className: classes?.root }}>\n {children}\n </Anchor>\n )\n}\n\nexport type PhoneLinkSlot = 'root' | 'icon' | 'label' | 'number'\nexport type PhoneLinkSkin = SkinEntry<PhoneLinkSlot, 'text' | 'button'>\n\nexport interface PhoneLinkProps extends PhoneLinkSkin {\n readonly phone: UiPhone\n /** Where the link sits, for `phone_link_click`. Required: a section sets its own. */\n readonly placement: PhoneCallPlacement\n /** Optional words before the number, for example \"Call\". The accessible name is always the visible text. */\n readonly label?: string\n /** The phone icon on the left (header and hero). */\n readonly showIcon?: boolean\n}\n\n/**\n * A `tel:` link. Its accessible name is its visible text, which always includes the number (WCAG 2.5.3). Use it for\n * every call link: a literal `tel:` anywhere else fails lint (K1) and C10 checks the href against the evidence file.\n */\nexport function PhoneLink({ phone, placement, label, showIcon = false, variant = 'text', classes }: PhoneLinkProps) {\n return (\n <a href={phone.telHref} data-bl=\"phone-link\" data-variant={variant} data-phone-location={placement} className={classes?.root}>\n {showIcon ? (\n <span {...slot(classes, 'icon')}>\n <Icon name=\"phone\" />\n </span>\n ) : null}\n {label ? (\n <>\n <span {...slot(classes, 'label')}>{label}</span>{' '}\n </>\n ) : null}\n <span {...slot(classes, 'number')}>{phone.display}</span>\n </a>\n )\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import type { LeadCtaPlacement } from '../../analytics.js';
2
+ import { type SkinEntry } from '../skin.js';
3
+ import type { ButtonVariant } from './links.js';
4
+ export type QuoteButtonSlot = 'root' | 'label' | 'icon';
5
+ export type QuoteButtonSkin = SkinEntry<QuoteButtonSlot, ButtonVariant, {
6
+ size?: 'regular' | 'large';
7
+ icon?: boolean;
8
+ }>;
9
+ export interface QuoteButtonProps extends QuoteButtonSkin {
10
+ /** The site's recorded quote label, or one from the closed list (C5), for example "Get a free quote". */
11
+ readonly label: string;
12
+ /** The quote form this falls back to without scripts, for example `/contact#quote`. */
13
+ readonly href: string;
14
+ /** Where the button sits, for `lead_cta_click`. */
15
+ readonly placement: LeadCtaPlacement;
16
+ /** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */
17
+ readonly ctaId: string;
18
+ }
19
+ export declare function QuoteButton({ label, href, placement, ctaId, variant, layout, classes }: QuoteButtonProps): import("react/jsx-runtime").JSX.Element;
20
+ //# sourceMappingURL=quote-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-button.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AACvD,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,eAAe,EAAE,aAAa,EAAE;IAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAEvH,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,yGAAyG;IACzG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAA;IACpC,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,gBAAgB,2CAmBpH"}
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so
4
+ * it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the
5
+ * contact page the link scrolls to the form. It carries the `data-bl-cta` hooks only: from 3.9.0 the shared
6
+ * instrumentation reads them and sends `lead_cta_click` when lead diagnostics are on. Nothing is sent in 3.8.0, and
7
+ * nothing here emits events itself.
8
+ */
9
+ import Link from 'next/link';
10
+ import { Icon } from '../icons.js';
11
+ import { slot } from '../skin.js';
12
+ export function QuoteButton({ label, href, placement, ctaId, variant = 'primary', layout, classes }) {
13
+ return (_jsxs(Link, { href: href, "data-bl": "quote-button", "data-variant": variant, "data-size": layout?.size ?? 'regular', "data-bl-cta": ctaId, "data-bl-cta-placement": placement, className: classes?.root, children: [_jsx("span", { ...slot(classes, 'label'), children: label }), layout?.icon === false ? null : (_jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "arrow" }) }))] }));
14
+ }
15
+ //# sourceMappingURL=quote-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote-button.js","sourceRoot":"","sources":["../../../src/ui-kit/primitives/quote-button.tsx"],"names":[],"mappings":";AAAA;;;;;;GAMG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAiBjD,MAAM,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,EAAoB;IACnH,OAAO,CACL,MAAC,IAAI,IACH,IAAI,EAAE,IAAI,aACF,cAAc,kBACR,OAAO,eACV,MAAM,EAAE,IAAI,IAAI,SAAS,iBACvB,KAAK,2BACK,SAAS,EAChC,SAAS,EAAE,OAAO,EAAE,IAAI,aAExB,kBAAU,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAAG,KAAK,GAAQ,EAC/C,MAAM,EAAE,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC/B,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,GAAG,GAChB,CACR,IACI,CACR,CAAA;AACH,CAAC","sourcesContent":["/**\n * `QuoteButton`: the quote action. A server-rendered link to the page's quote form (for example `/contact#quote`), so\n * it works before scripts run. When a `QuoteDialog` is on the page (3.10.0) it opens the dialog instead; on the\n * contact page the link scrolls to the form. It carries the `data-bl-cta` hooks only: from 3.9.0 the shared\n * instrumentation reads them and sends `lead_cta_click` when lead diagnostics are on. Nothing is sent in 3.8.0, and\n * nothing here emits events itself.\n */\nimport Link from 'next/link'\nimport type { LeadCtaPlacement } from '../../analytics.js'\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { ButtonVariant } from './links.js'\n\nexport type QuoteButtonSlot = 'root' | 'label' | 'icon'\nexport type QuoteButtonSkin = SkinEntry<QuoteButtonSlot, ButtonVariant, { size?: 'regular' | 'large'; icon?: boolean }>\n\nexport interface QuoteButtonProps extends QuoteButtonSkin {\n /** The site's recorded quote label, or one from the closed list (C5), for example \"Get a free quote\". */\n readonly label: string\n /** The quote form this falls back to without scripts, for example `/contact#quote`. */\n readonly href: string\n /** Where the button sits, for `lead_cta_click`. */\n readonly placement: LeadCtaPlacement\n /** A stable id for `lead_cta_click`, for example `quote-header` or `quote-cta`. */\n readonly ctaId: string\n}\n\nexport function QuoteButton({ label, href, placement, ctaId, variant = 'primary', layout, classes }: QuoteButtonProps) {\n return (\n <Link\n href={href}\n data-bl=\"quote-button\"\n data-variant={variant}\n data-size={layout?.size ?? 'regular'}\n data-bl-cta={ctaId}\n data-bl-cta-placement={placement}\n className={classes?.root}\n >\n <span {...slot(classes, 'label')}>{label}</span>\n {layout?.icon === false ? null : (\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow\" />\n </span>\n )}\n </Link>\n )\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export { cx, skinned, slot, type SkinEntry, type SkinEntryOf, type SkinnedProps, type SlotClasses } from './skin.js';
2
+ export type { BrandlabSkin } from './brandlab-skin.js';
3
+ //# sourceMappingURL=skin-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skin-index.d.ts","sourceRoot":"","sources":["../../src/ui-kit/skin-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AACpH,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { cx, skinned, slot } from './skin.js';
2
+ //# sourceMappingURL=skin-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skin-index.js","sourceRoot":"","sources":["../../src/ui-kit/skin-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAyE,MAAM,WAAW,CAAA","sourcesContent":["export { cx, skinned, slot, type SkinEntry, type SkinEntryOf, type SkinnedProps, type SlotClasses } from './skin.js'\nexport type { BrandlabSkin } from './brandlab-skin.js'\n"]}
@@ -0,0 +1,48 @@
1
+ /**
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.
4
+ * Colours, fonts and shapes come from the site's `--bl-*` tokens and these classes; behaviour, semantics, accessible
5
+ * names and analytics hooks never appear here, so a skin can't change them.
6
+ */
7
+ import { type ComponentType } from 'react';
8
+ /** One class string per named slot. Tailwind v4 utilities, CSS Module class names or nothing (plain CSS on hooks). */
9
+ 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>> {
12
+ readonly variant?: Variant;
13
+ readonly layout?: Readonly<Layout>;
14
+ readonly classes?: SlotClasses<Slot>;
15
+ }
16
+ type SkinKeys = 'variant' | 'layout' | 'classes';
17
+ /** The part of a component's props a skin entry supplies. */
18
+ export type SkinEntryOf<P> = Pick<P, Extract<keyof P, SkinKeys>>;
19
+ /** What a page passes to a skinned component: its data, and at most a `layout` adjustment within the contract. */
20
+ export type SkinnedProps<P> = Omit<P, 'variant' | 'classes'>;
21
+ /**
22
+ * Bind a component to the site's skin entry, so pages pass data only. Returns a plain function component (no
23
+ * context, no client code), so it works in server components and client components alike. The skin's layout merges
24
+ * under the page's `layout`; the variant and classes come from the skin only.
25
+ *
26
+ * ```tsx
27
+ * // src/site/ui.tsx
28
+ * export const ReviewsBand = skinned(UI.ReviewsBand, skin.reviewsBand)
29
+ * ```
30
+ */
31
+ export declare function skinned<P extends {
32
+ variant?: unknown;
33
+ layout?: object;
34
+ classes?: object;
35
+ }>(Component: ComponentType<P>, entry: SkinEntryOf<P>): ComponentType<SkinnedProps<P>>;
36
+ /** Join class strings, dropping empty parts. Returns `undefined` when nothing is left, so no empty `class` renders. */
37
+ export declare function cx(...parts: readonly (string | false | null | undefined)[]): string | undefined;
38
+ /**
39
+ * The stable hook for one named part: `data-slot` plus the skin's class for it. Spread it onto the element:
40
+ * `<div {...slot(classes, 'inner')}>`. Slot names, nesting and element types are public API (a minor release to
41
+ * change); a skin's class never replaces the hook.
42
+ */
43
+ export declare function slot<Slot extends string>(classes: SlotClasses<Slot> | undefined, name: Slot, extra?: string): {
44
+ 'data-slot': Slot;
45
+ className: string | undefined;
46
+ };
47
+ export {};
48
+ //# sourceMappingURL=skin.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,47 @@
1
+ /**
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.
4
+ * Colours, fonts and shapes come from the site's `--bl-*` tokens and these classes; behaviour, semantics, accessible
5
+ * names and analytics hooks never appear here, so a skin can't change them.
6
+ */
7
+ import { createElement } from 'react';
8
+ /**
9
+ * Bind a component to the site's skin entry, so pages pass data only. Returns a plain function component (no
10
+ * context, no client code), so it works in server components and client components alike. The skin's layout merges
11
+ * under the page's `layout`; the variant and classes come from the skin only.
12
+ *
13
+ * ```tsx
14
+ * // src/site/ui.tsx
15
+ * export const ReviewsBand = skinned(UI.ReviewsBand, skin.reviewsBand)
16
+ * ```
17
+ */
18
+ export function skinned(Component, entry) {
19
+ const skin = entry;
20
+ function Skinned(props) {
21
+ const pageLayout = props.layout;
22
+ const layout = skin.layout || pageLayout ? { ...(skin.layout ?? {}), ...(pageLayout ?? {}) } : undefined;
23
+ const merged = {
24
+ ...props,
25
+ ...(skin.variant !== undefined ? { variant: skin.variant } : {}),
26
+ ...(layout ? { layout } : {}),
27
+ ...(skin.classes ? { classes: skin.classes } : {}),
28
+ };
29
+ return createElement(Component, merged);
30
+ }
31
+ Skinned.displayName = `Skinned(${Component.displayName ?? Component.name ?? 'Component'})`;
32
+ return Skinned;
33
+ }
34
+ /** Join class strings, dropping empty parts. Returns `undefined` when nothing is left, so no empty `class` renders. */
35
+ export function cx(...parts) {
36
+ const joined = parts.filter(Boolean).join(' ');
37
+ return joined === '' ? undefined : joined;
38
+ }
39
+ /**
40
+ * The stable hook for one named part: `data-slot` plus the skin's class for it. Spread it onto the element:
41
+ * `<div {...slot(classes, 'inner')}>`. Slot names, nesting and element types are public API (a minor release to
42
+ * change); a skin's class never replaces the hook.
43
+ */
44
+ export function slot(classes, name, extra) {
45
+ return { 'data-slot': name, className: cx(classes?.[name], extra) };
46
+ }
47
+ //# sourceMappingURL=skin.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,123 @@
1
+ /* @brandlab-digital/site-next ui-kit structure.css: behaviour and layout only. Each site styles the components. */
2
+ @layer theme, base, bl-structure, components, utilities;
3
+
4
+ @layer bl-structure {
5
+ /* 00-base.css */
6
+ /* Base: scoped resets, focus, scroll padding, the visually-hidden utility and reduced motion. */
7
+
8
+ :where([data-bl], [data-bl-section]),
9
+ :where([data-bl], [data-bl-section]) *,
10
+ :where([data-bl], [data-bl-section]) *::before,
11
+ :where([data-bl], [data-bl-section]) *::after {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ :where([data-bl], [data-bl-section]) :where(svg) {
16
+ display: block;
17
+ flex-shrink: 0;
18
+ }
19
+
20
+ :where([data-bl], [data-bl-section]) :where([hidden]:not([hidden='until-found'])) {
21
+ display: none !important;
22
+ }
23
+
24
+ /* WCAG 2.4.11: a focused element is never hidden under the sticky header or the call bar. */
25
+ html {
26
+ scroll-padding-top: calc(var(--bl-header-height, 0px) + 0.5rem);
27
+ scroll-padding-bottom: calc(var(--bl-callbar-height, 0px) + 0.5rem);
28
+ }
29
+
30
+ :where([data-bl], [data-bl-section]) :focus-visible {
31
+ outline: 2px solid var(--bl-color-focus, currentColor);
32
+ outline-offset: 3px;
33
+ }
34
+
35
+ [data-bl-visually-hidden] {
36
+ position: absolute !important;
37
+ width: 1px !important;
38
+ height: 1px !important;
39
+ overflow: hidden !important;
40
+ clip-path: inset(50%) !important;
41
+ white-space: nowrap !important;
42
+ }
43
+
44
+ [data-bl='container'] {
45
+ width: 100%;
46
+ max-width: var(--bl-container, 80rem);
47
+ margin-inline: auto;
48
+ padding-inline: var(--bl-gutter, 1.25rem);
49
+ }
50
+ [data-bl='container'][data-width='narrow'] {
51
+ max-width: min(var(--bl-container, 80rem), 48rem);
52
+ }
53
+ [data-bl='container'][data-width='wide'] {
54
+ max-width: calc(var(--bl-container, 80rem) + 12rem);
55
+ }
56
+
57
+ [data-bl-section] {
58
+ position: relative;
59
+ padding-block: var(--bl-space-section, 4rem);
60
+ }
61
+ [data-bl-section] > [data-slot='inner'] {
62
+ position: relative;
63
+ max-width: var(--bl-container, 80rem);
64
+ margin-inline: auto;
65
+ padding-inline: var(--bl-gutter, 1.25rem);
66
+ }
67
+ [data-bl-section] > [data-slot='decoration'] {
68
+ position: absolute;
69
+ inset: 0;
70
+ pointer-events: none;
71
+ overflow: hidden;
72
+ }
73
+
74
+ /* WCAG 2.5.8 and the 44 px rule for primary actions. */
75
+ [data-bl='button'],
76
+ [data-bl='quote-button'] {
77
+ display: inline-flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ gap: 0.5rem;
81
+ min-height: 44px;
82
+ text-decoration: none;
83
+ }
84
+ [data-bl='phone-link'] {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ gap: 0.5rem;
88
+ min-height: 24px;
89
+ }
90
+ [data-bl='phone-link'][data-variant='button'] {
91
+ min-height: 44px;
92
+ }
93
+ :where([data-bl='button'], [data-bl='quote-button'], [data-bl='phone-link']) [data-slot='icon'] svg {
94
+ width: 1.25em;
95
+ height: 1.25em;
96
+ }
97
+
98
+ [data-bl='stars'] {
99
+ display: inline-flex;
100
+ gap: 0.125rem;
101
+ color: var(--bl-color-star, #f5b301);
102
+ }
103
+ [data-bl='stars'] [data-slot='star'] svg {
104
+ width: 1em;
105
+ height: 1em;
106
+ }
107
+ [data-bl='stars'] [data-slot='star'][data-state='off'] {
108
+ opacity: 0.3;
109
+ }
110
+
111
+ @media (prefers-reduced-motion: reduce) {
112
+ :where([data-bl], [data-bl-section]),
113
+ :where([data-bl], [data-bl-section]) *,
114
+ :where([data-bl], [data-bl-section]) *::before,
115
+ :where([data-bl], [data-bl-section]) *::after {
116
+ animation-duration: 0.01ms !important;
117
+ animation-iteration-count: 1 !important;
118
+ transition-duration: 0.01ms !important;
119
+ scroll-behavior: auto !important;
120
+ }
121
+ }
122
+
123
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Data shapes shared by the ui-kit components. A site passes data in these shapes; it never passes markup, ARIA or
3
+ * event props, so behaviour and semantics stay locked in the components.
4
+ */
5
+ /**
6
+ * A phone number ready to render: the number as the business shows it and its `tel:` href. Take both from the
7
+ * derived business facts (`deriveBusinessFacts(business).phone`), never type them by hand: C10 holds every `tel:` to
8
+ * the evidence-file phone. Mobiles and landlines use `tel:+61…`; 13, 1300 and 1800 numbers use their national digits.
9
+ */
10
+ export interface UiPhone {
11
+ readonly display: string;
12
+ readonly telHref: string;
13
+ }
14
+ /** A plain link. */
15
+ export interface UiLink {
16
+ readonly label: string;
17
+ readonly href: string;
18
+ }
19
+ export declare const UI_SOCIAL_KINDS: readonly ["facebook", "instagram", "google", "linkedin", "youtube", "tiktok"];
20
+ export type UiSocialKind = (typeof UI_SOCIAL_KINDS)[number];
21
+ /** A social or Business Profile link. `label` is the accessible name, for example "Abloom on Facebook". */
22
+ export interface UiSocial {
23
+ readonly kind: UiSocialKind;
24
+ readonly label: string;
25
+ readonly href: string;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ui-kit/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,oBAAoB;AACpB,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,eAAO,MAAM,eAAe,+EAAgF,CAAA;AAC5G,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAA;AAE3D,2GAA2G;AAC3G,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Data shapes shared by the ui-kit components. A site passes data in these shapes; it never passes markup, ARIA or
3
+ * event props, so behaviour and semantics stay locked in the components.
4
+ */
5
+ export const UI_SOCIAL_KINDS = ['facebook', 'instagram', 'google', 'linkedin', 'youtube', 'tiktok'];
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/ui-kit/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkBH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAU,CAAA","sourcesContent":["/**\n * Data shapes shared by the ui-kit components. A site passes data in these shapes; it never passes markup, ARIA or\n * event props, so behaviour and semantics stay locked in the components.\n */\n\n/**\n * A phone number ready to render: the number as the business shows it and its `tel:` href. Take both from the\n * derived business facts (`deriveBusinessFacts(business).phone`), never type them by hand: C10 holds every `tel:` to\n * the evidence-file phone. Mobiles and landlines use `tel:+61…`; 13, 1300 and 1800 numbers use their national digits.\n */\nexport interface UiPhone {\n readonly display: string\n readonly telHref: string\n}\n\n/** A plain link. */\nexport interface UiLink {\n readonly label: string\n readonly href: string\n}\n\nexport const UI_SOCIAL_KINDS = ['facebook', 'instagram', 'google', 'linkedin', 'youtube', 'tiktok'] as const\nexport type UiSocialKind = (typeof UI_SOCIAL_KINDS)[number]\n\n/** A social or Business Profile link. `label` is the accessible name, for example \"Abloom on Facebook\". */\nexport interface UiSocial {\n readonly kind: UiSocialKind\n readonly label: string\n readonly href: string\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandlab-digital/site-next",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,7 +10,8 @@
10
10
  "private": false,
11
11
  "type": "module",
12
12
  "sideEffects": [
13
- "./dist/image-runtime.js"
13
+ "./dist/image-runtime.js",
14
+ "./dist/ui-kit/structure.css"
14
15
  ],
15
16
  "files": [
16
17
  "dist"
@@ -19,12 +20,12 @@
19
20
  "access": "public"
20
21
  },
21
22
  "scripts": {
22
- "build": "rm -rf dist && tsc -p tsconfig.json",
23
+ "build": "rm -rf dist && tsc -p tsconfig.json && node scripts/build-structure-css.mjs",
23
24
  "test": "vitest run",
24
25
  "typecheck": "tsc --noEmit -p tsconfig.json"
25
26
  },
26
27
  "dependencies": {
27
- "@brandlab-digital/site-core": "3.7.0",
28
+ "@brandlab-digital/site-core": "3.8.0",
28
29
  "browser-image-compression": "2.0.2",
29
30
  "pdf-lib": "1.17.1",
30
31
  "server-only": "0.0.1",
@@ -152,6 +153,18 @@
152
153
  "./testing/server-only": {
153
154
  "types": "./dist/testing/server-only.d.ts",
154
155
  "import": "./dist/testing/server-only.js"
156
+ },
157
+ "./ui-kit/skin": {
158
+ "types": "./dist/ui-kit/skin-index.d.ts",
159
+ "import": "./dist/ui-kit/skin-index.js"
160
+ },
161
+ "./ui-kit/primitives": {
162
+ "types": "./dist/ui-kit/primitives/index.d.ts",
163
+ "import": "./dist/ui-kit/primitives/index.js"
164
+ },
165
+ "./ui-kit/structure.css": {
166
+ "style": "./dist/ui-kit/structure.css",
167
+ "default": "./dist/ui-kit/structure.css"
155
168
  }
156
169
  }
157
170
  }