@brandlab-digital/site-next 3.8.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/contact/attachments-client.d.ts +3 -0
- package/dist/contact/attachments-client.d.ts.map +1 -1
- package/dist/contact/attachments-client.js +31 -8
- package/dist/contact/attachments-client.js.map +1 -1
- package/dist/contact/client-entry.d.ts +1 -1
- package/dist/contact/client-entry.d.ts.map +1 -1
- package/dist/contact/client-entry.js.map +1 -1
- package/dist/contact/contract.d.ts.map +1 -1
- package/dist/contact/contract.js +2 -2
- package/dist/contact/contract.js.map +1 -1
- package/dist/contact/email.d.ts.map +1 -1
- package/dist/contact/email.js +9 -0
- package/dist/contact/email.js.map +1 -1
- package/dist/contact/form-client.d.ts +4 -2
- package/dist/contact/form-client.d.ts.map +1 -1
- package/dist/contact/form-client.js +49 -13
- package/dist/contact/form-client.js.map +1 -1
- package/dist/contact/rate-limit.d.ts +59 -0
- package/dist/contact/rate-limit.d.ts.map +1 -0
- package/dist/contact/rate-limit.js +109 -0
- package/dist/contact/rate-limit.js.map +1 -0
- package/dist/contact/route-handler.d.ts +6 -0
- package/dist/contact/route-handler.d.ts.map +1 -1
- package/dist/contact/route-handler.js +4 -0
- package/dist/contact/route-handler.js.map +1 -1
- package/dist/contact/server-entry.d.ts +1 -1
- package/dist/contact/server-entry.d.ts.map +1 -1
- package/dist/contact/server-entry.js +1 -1
- package/dist/contact/server-entry.js.map +1 -1
- package/dist/contact/server.d.ts +9 -0
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +71 -7
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/shared.d.ts +11 -0
- package/dist/contact/shared.d.ts.map +1 -1
- package/dist/contact/shared.js +11 -0
- package/dist/contact/shared.js.map +1 -1
- package/dist/contact/testing.d.ts +5 -0
- package/dist/contact/testing.d.ts.map +1 -1
- package/dist/contact/testing.js +1 -0
- package/dist/contact/testing.js.map +1 -1
- package/dist/instrumentation-client.d.ts +23 -4
- package/dist/instrumentation-client.d.ts.map +1 -1
- package/dist/instrumentation-client.js +65 -3
- package/dist/instrumentation-client.js.map +1 -1
- package/dist/lead-reporting.d.ts +6 -0
- package/dist/lead-reporting.d.ts.map +1 -1
- package/dist/lead-reporting.js +13 -1
- package/dist/lead-reporting.js.map +1 -1
- package/dist/navigation/constants.d.ts +25 -0
- package/dist/navigation/constants.d.ts.map +1 -0
- package/dist/navigation/constants.js +30 -0
- package/dist/navigation/constants.js.map +1 -0
- package/dist/navigation/index.d.ts +11 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +11 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/use-back-to-top.d.ts +5 -0
- package/dist/navigation/use-back-to-top.d.ts.map +1 -0
- package/dist/navigation/use-back-to-top.js +32 -0
- package/dist/navigation/use-back-to-top.js.map +1 -0
- package/dist/navigation/use-header-metrics.d.ts +17 -0
- package/dist/navigation/use-header-metrics.d.ts.map +1 -0
- package/dist/navigation/use-header-metrics.js +52 -0
- package/dist/navigation/use-header-metrics.js.map +1 -0
- package/dist/navigation/use-navigation-menu.d.ts +39 -0
- package/dist/navigation/use-navigation-menu.d.ts.map +1 -0
- package/dist/navigation/use-navigation-menu.js +206 -0
- package/dist/navigation/use-navigation-menu.js.map +1 -0
- package/dist/navigation/use-sticky-call-bar.d.ts +15 -0
- package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -0
- package/dist/navigation/use-sticky-call-bar.js +110 -0
- package/dist/navigation/use-sticky-call-bar.js.map +1 -0
- package/dist/ui-kit/brandlab-skin.d.ts +22 -0
- package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
- package/dist/ui-kit/brandlab-skin.js.map +1 -1
- package/dist/ui-kit/form/copy.d.ts +3 -0
- package/dist/ui-kit/form/copy.d.ts.map +1 -0
- package/dist/ui-kit/form/copy.js +5 -0
- package/dist/ui-kit/form/copy.js.map +1 -0
- package/dist/ui-kit/form/dialog.d.ts +24 -0
- package/dist/ui-kit/form/dialog.d.ts.map +1 -0
- package/dist/ui-kit/form/dialog.js +17 -0
- package/dist/ui-kit/form/dialog.js.map +1 -0
- package/dist/ui-kit/form/field.d.ts +30 -0
- package/dist/ui-kit/form/field.d.ts.map +1 -0
- package/dist/ui-kit/form/field.js +11 -0
- package/dist/ui-kit/form/field.js.map +1 -0
- package/dist/ui-kit/form/index.d.ts +7 -0
- package/dist/ui-kit/form/index.d.ts.map +1 -0
- package/dist/ui-kit/form/index.js +7 -0
- package/dist/ui-kit/form/index.js.map +1 -0
- package/dist/ui-kit/form/photo-upload.d.ts +37 -0
- package/dist/ui-kit/form/photo-upload.d.ts.map +1 -0
- package/dist/ui-kit/form/photo-upload.js +19 -0
- package/dist/ui-kit/form/photo-upload.js.map +1 -0
- package/dist/ui-kit/form/quote-dialog.d.ts +15 -0
- package/dist/ui-kit/form/quote-dialog.d.ts.map +1 -0
- package/dist/ui-kit/form/quote-dialog.js +68 -0
- package/dist/ui-kit/form/quote-dialog.js.map +1 -0
- package/dist/ui-kit/form/quote-form.d.ts +165 -0
- package/dist/ui-kit/form/quote-form.d.ts.map +1 -0
- package/dist/ui-kit/form/quote-form.js +235 -0
- package/dist/ui-kit/form/quote-form.js.map +1 -0
- package/dist/ui-kit/form/thank-you.d.ts +37 -0
- package/dist/ui-kit/form/thank-you.d.ts.map +1 -0
- package/dist/ui-kit/form/thank-you.js +36 -0
- package/dist/ui-kit/form/thank-you.js.map +1 -0
- package/dist/ui-kit/shell/back-to-top-client.d.ts +9 -0
- package/dist/ui-kit/shell/back-to-top-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/back-to-top-client.js +8 -0
- package/dist/ui-kit/shell/back-to-top-client.js.map +1 -0
- package/dist/ui-kit/shell/back-to-top.d.ts +13 -0
- package/dist/ui-kit/shell/back-to-top.d.ts.map +1 -0
- package/dist/ui-kit/shell/back-to-top.js +14 -0
- package/dist/ui-kit/shell/back-to-top.js.map +1 -0
- package/dist/ui-kit/shell/call-bar-client.d.ts +7 -0
- package/dist/ui-kit/shell/call-bar-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/call-bar-client.js +13 -0
- package/dist/ui-kit/shell/call-bar-client.js.map +1 -0
- package/dist/ui-kit/shell/current-link.d.ts +15 -0
- package/dist/ui-kit/shell/current-link.d.ts.map +1 -0
- package/dist/ui-kit/shell/current-link.js +34 -0
- package/dist/ui-kit/shell/current-link.js.map +1 -0
- package/dist/ui-kit/shell/drawer-client.d.ts +22 -0
- package/dist/ui-kit/shell/drawer-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/drawer-client.js +70 -0
- package/dist/ui-kit/shell/drawer-client.js.map +1 -0
- package/dist/ui-kit/shell/header-client.d.ts +44 -0
- package/dist/ui-kit/shell/header-client.d.ts.map +1 -0
- package/dist/ui-kit/shell/header-client.js +59 -0
- package/dist/ui-kit/shell/header-client.js.map +1 -0
- package/dist/ui-kit/shell/index.d.ts +6 -0
- package/dist/ui-kit/shell/index.d.ts.map +1 -0
- package/dist/ui-kit/shell/index.js +6 -0
- package/dist/ui-kit/shell/index.js.map +1 -0
- package/dist/ui-kit/shell/mobile-call-bar.d.ts +16 -0
- package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -0
- package/dist/ui-kit/shell/mobile-call-bar.js +15 -0
- package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -0
- package/dist/ui-kit/shell/mobile-drawer.d.ts +37 -0
- package/dist/ui-kit/shell/mobile-drawer.d.ts.map +1 -0
- package/dist/ui-kit/shell/mobile-drawer.js +25 -0
- package/dist/ui-kit/shell/mobile-drawer.js.map +1 -0
- package/dist/ui-kit/shell/shell-context.d.ts +3 -0
- package/dist/ui-kit/shell/shell-context.d.ts.map +1 -0
- package/dist/ui-kit/shell/shell-context.js +5 -0
- package/dist/ui-kit/shell/shell-context.js.map +1 -0
- package/dist/ui-kit/shell/site-header.d.ts +52 -0
- package/dist/ui-kit/shell/site-header.d.ts.map +1 -0
- package/dist/ui-kit/shell/site-header.js +36 -0
- package/dist/ui-kit/shell/site-header.js.map +1 -0
- package/dist/ui-kit/shell/social-links.d.ts +8 -0
- package/dist/ui-kit/shell/social-links.d.ts.map +1 -0
- package/dist/ui-kit/shell/social-links.js +9 -0
- package/dist/ui-kit/shell/social-links.js.map +1 -0
- package/dist/ui-kit/shell/utility-bar.d.ts +30 -0
- package/dist/ui-kit/shell/utility-bar.d.ts.map +1 -0
- package/dist/ui-kit/shell/utility-bar.js +15 -0
- package/dist/ui-kit/shell/utility-bar.js.map +1 -0
- package/dist/ui-kit/structure.css +791 -0
- package/dist/ui.d.ts +10 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +3 -3
- package/dist/ui.js.map +1 -1
- package/package.json +14 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* F2: the header's measured state. The mode follows the header's own width (so a framed specimen behaves like a real
|
|
4
|
+
* window): 1280 px and up `desktop`, 1024 to 1279 px `compact`, below 1024 px `phone`. The hook writes the frozen
|
|
5
|
+
* hooks on the header root (`data-mode`, `data-hydrated`, `data-compact`) and `--bl-header-height` on the document,
|
|
6
|
+
* which drives `scroll-padding-top` (WCAG 2.4.11).
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { HEADER_COMPACT_AFTER_SCROLL_PX, headerModeFor } from './constants.js';
|
|
10
|
+
export function useHeaderMetrics(rootRef, options = {}) {
|
|
11
|
+
const compactAfterScroll = options.compactAfterScroll ?? false;
|
|
12
|
+
const [metrics, setMetrics] = useState({ mode: null, hydrated: false });
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const root = rootRef.current;
|
|
15
|
+
if (!root)
|
|
16
|
+
return;
|
|
17
|
+
const html = document.documentElement;
|
|
18
|
+
const measure = () => {
|
|
19
|
+
const box = root.getBoundingClientRect();
|
|
20
|
+
const mode = headerModeFor(box.width);
|
|
21
|
+
root.dataset.mode = mode;
|
|
22
|
+
html.style.setProperty('--bl-header-height', `${Math.round(box.height)}px`);
|
|
23
|
+
setMetrics((current) => (current.mode === mode && current.hydrated ? current : { mode, hydrated: true }));
|
|
24
|
+
};
|
|
25
|
+
const onScroll = () => {
|
|
26
|
+
root.dataset.compact = window.scrollY > HEADER_COMPACT_AFTER_SCROLL_PX ? 'true' : 'false';
|
|
27
|
+
};
|
|
28
|
+
root.dataset.hydrated = 'true';
|
|
29
|
+
measure();
|
|
30
|
+
let observer;
|
|
31
|
+
if (typeof ResizeObserver === 'function') {
|
|
32
|
+
observer = new ResizeObserver(measure);
|
|
33
|
+
observer.observe(root);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
window.addEventListener('resize', measure);
|
|
37
|
+
}
|
|
38
|
+
if (compactAfterScroll) {
|
|
39
|
+
onScroll();
|
|
40
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
41
|
+
}
|
|
42
|
+
return () => {
|
|
43
|
+
observer?.disconnect();
|
|
44
|
+
window.removeEventListener('resize', measure);
|
|
45
|
+
window.removeEventListener('scroll', onScroll);
|
|
46
|
+
delete root.dataset.compact;
|
|
47
|
+
html.style.removeProperty('--bl-header-height');
|
|
48
|
+
};
|
|
49
|
+
}, [rootRef, compactAfterScroll]);
|
|
50
|
+
return metrics;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=use-header-metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-header-metrics.js","sourceRoot":"","sources":["../../src/navigation/use-header-metrics.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ;;;;;GAKG;AACH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAA;AAC3D,OAAO,EAAE,8BAA8B,EAAE,aAAa,EAAmB,MAAM,gBAAgB,CAAA;AAQ/F,MAAM,UAAU,gBAAgB,CAAC,OAAsC,EAAE,UAA4C,EAAE;IACrH,MAAM,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,IAAI,KAAK,CAAA;IAC9D,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;IAEtF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAA;QAC5B,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAA;QACrC,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,MAAM,GAAG,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAA;YACxC,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACrC,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAA;YACxB,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,oBAAoB,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YAC3E,UAAU,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;QAC3G,CAAC,CAAA;QACD,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,IAAI,CAAC,OAAO,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,GAAG,8BAA8B,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;QAC3F,CAAC,CAAA;QACD,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAA;QAC9B,OAAO,EAAE,CAAA;QACT,IAAI,QAAoC,CAAA;QACxC,IAAI,OAAO,cAAc,KAAK,UAAU,EAAE,CAAC;YACzC,QAAQ,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,CAAA;YACtC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC5C,CAAC;QACD,IAAI,kBAAkB,EAAE,CAAC;YACvB,QAAQ,EAAE,CAAA;YACV,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;QAChE,CAAC;QACD,OAAO,GAAG,EAAE;YACV,QAAQ,EAAE,UAAU,EAAE,CAAA;YACtB,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;YAC7C,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;YAC9C,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAA;YAC3B,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,oBAAoB,CAAC,CAAA;QACjD,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAA;IAEjC,OAAO,OAAO,CAAA;AAChB,CAAC","sourcesContent":["'use client'\n\n/**\n * F2: the header's measured state. The mode follows the header's own width (so a framed specimen behaves like a real\n * window): 1280 px and up `desktop`, 1024 to 1279 px `compact`, below 1024 px `phone`. The hook writes the frozen\n * hooks on the header root (`data-mode`, `data-hydrated`, `data-compact`) and `--bl-header-height` on the document,\n * which drives `scroll-padding-top` (WCAG 2.4.11).\n */\nimport { useEffect, useState, type RefObject } from 'react'\nimport { HEADER_COMPACT_AFTER_SCROLL_PX, headerModeFor, type HeaderMode } from './constants.js'\n\nexport interface HeaderMetrics {\n /** Null until the header has been measured after hydration. */\n readonly mode: HeaderMode | null\n readonly hydrated: boolean\n}\n\nexport function useHeaderMetrics(rootRef: RefObject<HTMLElement | null>, options: { compactAfterScroll?: boolean } = {}): HeaderMetrics {\n const compactAfterScroll = options.compactAfterScroll ?? false\n const [metrics, setMetrics] = useState<HeaderMetrics>({ mode: null, hydrated: false })\n\n useEffect(() => {\n const root = rootRef.current\n if (!root) return\n const html = document.documentElement\n const measure = () => {\n const box = root.getBoundingClientRect()\n const mode = headerModeFor(box.width)\n root.dataset.mode = mode\n html.style.setProperty('--bl-header-height', `${Math.round(box.height)}px`)\n setMetrics((current) => (current.mode === mode && current.hydrated ? current : { mode, hydrated: true }))\n }\n const onScroll = () => {\n root.dataset.compact = window.scrollY > HEADER_COMPACT_AFTER_SCROLL_PX ? 'true' : 'false'\n }\n root.dataset.hydrated = 'true'\n measure()\n let observer: ResizeObserver | undefined\n if (typeof ResizeObserver === 'function') {\n observer = new ResizeObserver(measure)\n observer.observe(root)\n } else {\n window.addEventListener('resize', measure)\n }\n if (compactAfterScroll) {\n onScroll()\n window.addEventListener('scroll', onScroll, { passive: true })\n }\n return () => {\n observer?.disconnect()\n window.removeEventListener('resize', measure)\n window.removeEventListener('scroll', onScroll)\n delete root.dataset.compact\n html.style.removeProperty('--bl-header-height')\n }\n }, [rootRef, compactAfterScroll])\n\n return metrics\n}\n"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F3: the dropdown engine. The WAI-ARIA disclosure navigation pattern with top-level links and separate disclosure
|
|
3
|
+
* buttons, and the whole v1 §5.6 state table. Timings and rules are constants of the library; a site can't change
|
|
4
|
+
* them. The engine reads the DOM by hooks only (`data-slot="trigger"` and `data-slot="panel"` with `data-menu-id`),
|
|
5
|
+
* so the links and panels stay server-rendered.
|
|
6
|
+
*/
|
|
7
|
+
import { type FocusEvent, type KeyboardEvent, type MouseEvent, type PointerEvent, type RefObject } from 'react';
|
|
8
|
+
export type NavigationOpenedBy = 'hover' | 'click' | 'touch' | 'keyboard';
|
|
9
|
+
export interface NavigationMenu {
|
|
10
|
+
/** The open panel's menu id, or null. One panel is open at a time. */
|
|
11
|
+
readonly openId: string | null;
|
|
12
|
+
readonly openedBy: NavigationOpenedBy | null;
|
|
13
|
+
close(): void;
|
|
14
|
+
/** Spread on the item (`<li>`): hover intent and Tab out. */
|
|
15
|
+
itemHandlers(id: string): {
|
|
16
|
+
onPointerEnter(event: PointerEvent): void;
|
|
17
|
+
onPointerLeave(event: PointerEvent): void;
|
|
18
|
+
onPointerDown(event: PointerEvent): void;
|
|
19
|
+
onBlur(event: FocusEvent<HTMLElement>): void;
|
|
20
|
+
};
|
|
21
|
+
/** Spread on the disclosure button: click, Enter and Space toggle; Arrow Down opens and enters the panel. */
|
|
22
|
+
triggerHandlers(id: string): {
|
|
23
|
+
onClick(event: MouseEvent<HTMLButtonElement>): void;
|
|
24
|
+
onKeyDown(event: KeyboardEvent<HTMLButtonElement>): void;
|
|
25
|
+
};
|
|
26
|
+
/** Spread on the panel: Arrow Up, Arrow Down, Home and End move between its links; a link click closes. */
|
|
27
|
+
panelHandlers(id: string): {
|
|
28
|
+
onKeyDown(event: KeyboardEvent<HTMLElement>): void;
|
|
29
|
+
onClick(event: MouseEvent<HTMLElement>): void;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The menu state for one navigation. `navRef` is the `<nav>`; a change of `resetKey` (route or breakpoint) closes
|
|
34
|
+
* every panel, as does `popstate`, Escape (focus returns to the chevron), a pointer down outside, Tab out of an item
|
|
35
|
+
* and a link click. A mouse opens a panel 100 ms after it enters an item and closes it 200 ms after it leaves, even
|
|
36
|
+
* after a click; the first click on a hover-opened chevron confirms the panel instead of closing it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function useNavigationMenu(navRef: RefObject<HTMLElement | null>, resetKey: unknown): NavigationMenu;
|
|
39
|
+
//# sourceMappingURL=use-navigation-menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-navigation-menu.d.ts","sourceRoot":"","sources":["../../src/navigation/use-navigation-menu.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,OAAO,EAA4C,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAGzJ,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAA;AAezE,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC5C,KAAK,IAAI,IAAI,CAAA;IACb,6DAA6D;IAC7D,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG;QACxB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;QACzC,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;QACzC,aAAa,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;QACxC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,IAAI,CAAA;KAC7C,CAAA;IACD,6GAA6G;IAC7G,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG;QAC3B,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAA;QACnD,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAA;KACzD,CAAA;IACD,2GAA2G;IAC3G,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG;QACzB,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,IAAI,CAAA;QAClD,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,GAAG,IAAI,CAAA;KAC9C,CAAA;CACF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,cAAc,CA6K1G"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* F3: the dropdown engine. The WAI-ARIA disclosure navigation pattern with top-level links and separate disclosure
|
|
4
|
+
* buttons, and the whole v1 §5.6 state table. Timings and rules are constants of the library; a site can't change
|
|
5
|
+
* them. The engine reads the DOM by hooks only (`data-slot="trigger"` and `data-slot="panel"` with `data-menu-id`),
|
|
6
|
+
* so the links and panels stay server-rendered.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { NAV_HOVER_CLOSE_DELAY_MS, NAV_HOVER_MEDIA_QUERY, NAV_HOVER_OPEN_DELAY_MS } from './constants.js';
|
|
10
|
+
const CLOSED = { id: null, by: null };
|
|
11
|
+
/** "Mouse" means pointerType mouse on a (hover: hover) and (pointer: fine) device. Touch never opens on hover. */
|
|
12
|
+
function isMouse(event) {
|
|
13
|
+
if (event.pointerType !== 'mouse')
|
|
14
|
+
return false;
|
|
15
|
+
return typeof window.matchMedia === 'function' && window.matchMedia(NAV_HOVER_MEDIA_QUERY).matches;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The menu state for one navigation. `navRef` is the `<nav>`; a change of `resetKey` (route or breakpoint) closes
|
|
19
|
+
* every panel, as does `popstate`, Escape (focus returns to the chevron), a pointer down outside, Tab out of an item
|
|
20
|
+
* and a link click. A mouse opens a panel 100 ms after it enters an item and closes it 200 ms after it leaves, even
|
|
21
|
+
* after a click; the first click on a hover-opened chevron confirms the panel instead of closing it.
|
|
22
|
+
*/
|
|
23
|
+
export function useNavigationMenu(navRef, resetKey) {
|
|
24
|
+
const [state, setState] = useState(CLOSED);
|
|
25
|
+
const stateRef = useRef(state);
|
|
26
|
+
const openTimer = useRef(null);
|
|
27
|
+
const closeTimer = useRef(null);
|
|
28
|
+
const lastPointerType = useRef('mouse');
|
|
29
|
+
const focusFirstOf = useRef(null);
|
|
30
|
+
const commit = useCallback((next) => {
|
|
31
|
+
stateRef.current = next;
|
|
32
|
+
setState(next);
|
|
33
|
+
}, []);
|
|
34
|
+
const clearTimers = useCallback(() => {
|
|
35
|
+
if (openTimer.current !== null)
|
|
36
|
+
window.clearTimeout(openTimer.current);
|
|
37
|
+
if (closeTimer.current !== null)
|
|
38
|
+
window.clearTimeout(closeTimer.current);
|
|
39
|
+
openTimer.current = null;
|
|
40
|
+
closeTimer.current = null;
|
|
41
|
+
}, []);
|
|
42
|
+
const close = useCallback(() => {
|
|
43
|
+
clearTimers();
|
|
44
|
+
if (stateRef.current.id !== null)
|
|
45
|
+
commit(CLOSED);
|
|
46
|
+
}, [clearTimers, commit]);
|
|
47
|
+
const trigger = useCallback((id) => navRef.current?.querySelector(`[data-slot="trigger"][data-menu-id="${id}"]`) ?? null, [navRef]);
|
|
48
|
+
const panelLinks = useCallback((id) => Array.from(navRef.current?.querySelectorAll(`[data-slot="panel"][data-menu-id="${id}"] a[href]`) ?? []), [navRef]);
|
|
49
|
+
// Arrow Down opened a panel: focus its first link once the panel is no longer inert.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (state.id && focusFirstOf.current === state.id) {
|
|
52
|
+
focusFirstOf.current = null;
|
|
53
|
+
panelLinks(state.id)[0]?.focus();
|
|
54
|
+
}
|
|
55
|
+
}, [state, panelLinks]);
|
|
56
|
+
// Escape closes and returns focus to the chevron; a pointer down outside closes.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!state.id)
|
|
59
|
+
return;
|
|
60
|
+
const onKey = (event) => {
|
|
61
|
+
if (event.key !== 'Escape' || !stateRef.current.id)
|
|
62
|
+
return;
|
|
63
|
+
const id = stateRef.current.id;
|
|
64
|
+
const item = trigger(id)?.closest('[data-slot="item"]');
|
|
65
|
+
const focusInside = item?.contains(document.activeElement) ?? false;
|
|
66
|
+
close();
|
|
67
|
+
if (focusInside)
|
|
68
|
+
trigger(id)?.focus();
|
|
69
|
+
};
|
|
70
|
+
const onPointerDown = (event) => {
|
|
71
|
+
if (!navRef.current?.contains(event.target))
|
|
72
|
+
close();
|
|
73
|
+
};
|
|
74
|
+
document.addEventListener('keydown', onKey);
|
|
75
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
76
|
+
return () => {
|
|
77
|
+
document.removeEventListener('keydown', onKey);
|
|
78
|
+
document.removeEventListener('pointerdown', onPointerDown);
|
|
79
|
+
};
|
|
80
|
+
}, [state.id, close, navRef, trigger]);
|
|
81
|
+
// A route change, popstate or a breakpoint crossing (resetKey) closes every panel.
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
close();
|
|
84
|
+
}, [resetKey, close]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const onPop = () => close();
|
|
87
|
+
window.addEventListener('popstate', onPop);
|
|
88
|
+
return () => {
|
|
89
|
+
window.removeEventListener('popstate', onPop);
|
|
90
|
+
clearTimers();
|
|
91
|
+
};
|
|
92
|
+
}, [close, clearTimers]);
|
|
93
|
+
const itemHandlers = (id) => ({
|
|
94
|
+
onPointerEnter: (event) => {
|
|
95
|
+
lastPointerType.current = event.pointerType;
|
|
96
|
+
if (!isMouse(event))
|
|
97
|
+
return;
|
|
98
|
+
if (closeTimer.current !== null) {
|
|
99
|
+
window.clearTimeout(closeTimer.current);
|
|
100
|
+
closeTimer.current = null;
|
|
101
|
+
}
|
|
102
|
+
const current = stateRef.current;
|
|
103
|
+
if (current.id === id)
|
|
104
|
+
return;
|
|
105
|
+
if (current.id !== null) {
|
|
106
|
+
// Another panel is open: switch immediately.
|
|
107
|
+
commit({ id, by: 'hover' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (openTimer.current !== null)
|
|
111
|
+
window.clearTimeout(openTimer.current);
|
|
112
|
+
openTimer.current = window.setTimeout(() => {
|
|
113
|
+
openTimer.current = null;
|
|
114
|
+
commit({ id, by: 'hover' });
|
|
115
|
+
}, NAV_HOVER_OPEN_DELAY_MS);
|
|
116
|
+
},
|
|
117
|
+
onPointerLeave: (event) => {
|
|
118
|
+
if (!isMouse(event))
|
|
119
|
+
return;
|
|
120
|
+
if (openTimer.current !== null) {
|
|
121
|
+
window.clearTimeout(openTimer.current);
|
|
122
|
+
openTimer.current = null;
|
|
123
|
+
}
|
|
124
|
+
// Applies even after a click or focus inside: no pinned-open state for mouse users.
|
|
125
|
+
if (stateRef.current.id === id) {
|
|
126
|
+
if (closeTimer.current !== null)
|
|
127
|
+
window.clearTimeout(closeTimer.current);
|
|
128
|
+
closeTimer.current = window.setTimeout(() => {
|
|
129
|
+
closeTimer.current = null;
|
|
130
|
+
commit(CLOSED);
|
|
131
|
+
}, NAV_HOVER_CLOSE_DELAY_MS);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
onPointerDown: (event) => {
|
|
135
|
+
lastPointerType.current = event.pointerType;
|
|
136
|
+
},
|
|
137
|
+
onBlur: (event) => {
|
|
138
|
+
// Tab out of the item closes it.
|
|
139
|
+
const next = event.relatedTarget;
|
|
140
|
+
if (next && event.currentTarget.contains(next))
|
|
141
|
+
return;
|
|
142
|
+
if (next && stateRef.current.id === id)
|
|
143
|
+
close();
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const triggerHandlers = (id) => ({
|
|
147
|
+
onClick: (event) => {
|
|
148
|
+
const current = stateRef.current;
|
|
149
|
+
const keyboard = event.detail === 0;
|
|
150
|
+
clearTimers();
|
|
151
|
+
if (current.id === id) {
|
|
152
|
+
// The first mouse click confirms a hover-opened panel; otherwise a click toggles it closed.
|
|
153
|
+
if (!keyboard && current.by === 'hover')
|
|
154
|
+
commit({ id, by: 'click' });
|
|
155
|
+
else
|
|
156
|
+
commit(CLOSED);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const touch = lastPointerType.current === 'touch' || lastPointerType.current === 'pen';
|
|
160
|
+
commit({ id, by: keyboard ? 'keyboard' : touch ? 'touch' : 'click' });
|
|
161
|
+
},
|
|
162
|
+
onKeyDown: (event) => {
|
|
163
|
+
if (event.key !== 'ArrowDown')
|
|
164
|
+
return;
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
clearTimers();
|
|
167
|
+
if (stateRef.current.id === id) {
|
|
168
|
+
panelLinks(id)[0]?.focus();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
focusFirstOf.current = id;
|
|
172
|
+
commit({ id, by: 'keyboard' });
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const panelHandlers = (id) => ({
|
|
176
|
+
onKeyDown: (event) => {
|
|
177
|
+
const links = panelLinks(id);
|
|
178
|
+
const index = links.indexOf(document.activeElement);
|
|
179
|
+
if (index === -1)
|
|
180
|
+
return;
|
|
181
|
+
let next = -1;
|
|
182
|
+
if (event.key === 'ArrowDown')
|
|
183
|
+
next = Math.min(links.length - 1, index + 1);
|
|
184
|
+
else if (event.key === 'ArrowUp')
|
|
185
|
+
next = index - 1;
|
|
186
|
+
else if (event.key === 'Home')
|
|
187
|
+
next = 0;
|
|
188
|
+
else if (event.key === 'End')
|
|
189
|
+
next = links.length - 1;
|
|
190
|
+
else
|
|
191
|
+
return;
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
if (next < 0)
|
|
194
|
+
trigger(id)?.focus();
|
|
195
|
+
else
|
|
196
|
+
links[next]?.focus();
|
|
197
|
+
},
|
|
198
|
+
// A link in the panel is clicked: close.
|
|
199
|
+
onClick: (event) => {
|
|
200
|
+
if (event.target.closest('a[href]'))
|
|
201
|
+
close();
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return { openId: state.id, openedBy: state.by, close, itemHandlers, triggerHandlers, panelHandlers };
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=use-navigation-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-navigation-menu.js","sourceRoot":"","sources":["../../src/navigation/use-navigation-menu.ts"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * F3: the dropdown engine. The WAI-ARIA disclosure navigation pattern with top-level links and separate disclosure\n * buttons, and the whole v1 §5.6 state table. Timings and rules are constants of the library; a site can't change\n * them. The engine reads the DOM by hooks only (`data-slot=\"trigger\"` and `data-slot=\"panel\"` with `data-menu-id`),\n * so the links and panels stay server-rendered.\n */\nimport { useCallback, useEffect, useRef, useState, type FocusEvent, type KeyboardEvent, type MouseEvent, type PointerEvent, type RefObject } from 'react'\nimport { NAV_HOVER_CLOSE_DELAY_MS, NAV_HOVER_MEDIA_QUERY, NAV_HOVER_OPEN_DELAY_MS } from './constants.js'\n\nexport type NavigationOpenedBy = 'hover' | 'click' | 'touch' | 'keyboard'\n\ninterface MenuState {\n readonly id: string | null\n readonly by: NavigationOpenedBy | null\n}\n\nconst CLOSED: MenuState = { id: null, by: null }\n\n/** \"Mouse\" means pointerType mouse on a (hover: hover) and (pointer: fine) device. Touch never opens on hover. */\nfunction isMouse(event: PointerEvent): boolean {\n if (event.pointerType !== 'mouse') return false\n return typeof window.matchMedia === 'function' && window.matchMedia(NAV_HOVER_MEDIA_QUERY).matches\n}\n\nexport interface NavigationMenu {\n /** The open panel's menu id, or null. One panel is open at a time. */\n readonly openId: string | null\n readonly openedBy: NavigationOpenedBy | null\n close(): void\n /** Spread on the item (`<li>`): hover intent and Tab out. */\n itemHandlers(id: string): {\n onPointerEnter(event: PointerEvent): void\n onPointerLeave(event: PointerEvent): void\n onPointerDown(event: PointerEvent): void\n onBlur(event: FocusEvent<HTMLElement>): void\n }\n /** Spread on the disclosure button: click, Enter and Space toggle; Arrow Down opens and enters the panel. */\n triggerHandlers(id: string): {\n onClick(event: MouseEvent<HTMLButtonElement>): void\n onKeyDown(event: KeyboardEvent<HTMLButtonElement>): void\n }\n /** Spread on the panel: Arrow Up, Arrow Down, Home and End move between its links; a link click closes. */\n panelHandlers(id: string): {\n onKeyDown(event: KeyboardEvent<HTMLElement>): void\n onClick(event: MouseEvent<HTMLElement>): void\n }\n}\n\n/**\n * The menu state for one navigation. `navRef` is the `<nav>`; a change of `resetKey` (route or breakpoint) closes\n * every panel, as does `popstate`, Escape (focus returns to the chevron), a pointer down outside, Tab out of an item\n * and a link click. A mouse opens a panel 100 ms after it enters an item and closes it 200 ms after it leaves, even\n * after a click; the first click on a hover-opened chevron confirms the panel instead of closing it.\n */\nexport function useNavigationMenu(navRef: RefObject<HTMLElement | null>, resetKey: unknown): NavigationMenu {\n const [state, setState] = useState<MenuState>(CLOSED)\n const stateRef = useRef(state)\n const openTimer = useRef<number | null>(null)\n const closeTimer = useRef<number | null>(null)\n const lastPointerType = useRef<string>('mouse')\n const focusFirstOf = useRef<string | null>(null)\n\n const commit = useCallback((next: MenuState) => {\n stateRef.current = next\n setState(next)\n }, [])\n\n const clearTimers = useCallback(() => {\n if (openTimer.current !== null) window.clearTimeout(openTimer.current)\n if (closeTimer.current !== null) window.clearTimeout(closeTimer.current)\n openTimer.current = null\n closeTimer.current = null\n }, [])\n\n const close = useCallback(() => {\n clearTimers()\n if (stateRef.current.id !== null) commit(CLOSED)\n }, [clearTimers, commit])\n\n const trigger = useCallback(\n (id: string) => navRef.current?.querySelector<HTMLButtonElement>(`[data-slot=\"trigger\"][data-menu-id=\"${id}\"]`) ?? null,\n [navRef],\n )\n const panelLinks = useCallback(\n (id: string) => Array.from(navRef.current?.querySelectorAll<HTMLAnchorElement>(`[data-slot=\"panel\"][data-menu-id=\"${id}\"] a[href]`) ?? []),\n [navRef],\n )\n\n // Arrow Down opened a panel: focus its first link once the panel is no longer inert.\n useEffect(() => {\n if (state.id && focusFirstOf.current === state.id) {\n focusFirstOf.current = null\n panelLinks(state.id)[0]?.focus()\n }\n }, [state, panelLinks])\n\n // Escape closes and returns focus to the chevron; a pointer down outside closes.\n useEffect(() => {\n if (!state.id) return\n const onKey = (event: globalThis.KeyboardEvent) => {\n if (event.key !== 'Escape' || !stateRef.current.id) return\n const id = stateRef.current.id\n const item = trigger(id)?.closest('[data-slot=\"item\"]')\n const focusInside = item?.contains(document.activeElement) ?? false\n close()\n if (focusInside) trigger(id)?.focus()\n }\n const onPointerDown = (event: globalThis.PointerEvent) => {\n if (!navRef.current?.contains(event.target as Node)) close()\n }\n document.addEventListener('keydown', onKey)\n document.addEventListener('pointerdown', onPointerDown)\n return () => {\n document.removeEventListener('keydown', onKey)\n document.removeEventListener('pointerdown', onPointerDown)\n }\n }, [state.id, close, navRef, trigger])\n\n // A route change, popstate or a breakpoint crossing (resetKey) closes every panel.\n useEffect(() => {\n close()\n }, [resetKey, close])\n useEffect(() => {\n const onPop = () => close()\n window.addEventListener('popstate', onPop)\n return () => {\n window.removeEventListener('popstate', onPop)\n clearTimers()\n }\n }, [close, clearTimers])\n\n const itemHandlers = (id: string) => ({\n onPointerEnter: (event: PointerEvent) => {\n lastPointerType.current = event.pointerType\n if (!isMouse(event)) return\n if (closeTimer.current !== null) {\n window.clearTimeout(closeTimer.current)\n closeTimer.current = null\n }\n const current = stateRef.current\n if (current.id === id) return\n if (current.id !== null) {\n // Another panel is open: switch immediately.\n commit({ id, by: 'hover' })\n return\n }\n if (openTimer.current !== null) window.clearTimeout(openTimer.current)\n openTimer.current = window.setTimeout(() => {\n openTimer.current = null\n commit({ id, by: 'hover' })\n }, NAV_HOVER_OPEN_DELAY_MS)\n },\n onPointerLeave: (event: PointerEvent) => {\n if (!isMouse(event)) return\n if (openTimer.current !== null) {\n window.clearTimeout(openTimer.current)\n openTimer.current = null\n }\n // Applies even after a click or focus inside: no pinned-open state for mouse users.\n if (stateRef.current.id === id) {\n if (closeTimer.current !== null) window.clearTimeout(closeTimer.current)\n closeTimer.current = window.setTimeout(() => {\n closeTimer.current = null\n commit(CLOSED)\n }, NAV_HOVER_CLOSE_DELAY_MS)\n }\n },\n onPointerDown: (event: PointerEvent) => {\n lastPointerType.current = event.pointerType\n },\n onBlur: (event: FocusEvent<HTMLElement>) => {\n // Tab out of the item closes it.\n const next = event.relatedTarget as Node | null\n if (next && event.currentTarget.contains(next)) return\n if (next && stateRef.current.id === id) close()\n },\n })\n\n const triggerHandlers = (id: string) => ({\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n const current = stateRef.current\n const keyboard = event.detail === 0\n clearTimers()\n if (current.id === id) {\n // The first mouse click confirms a hover-opened panel; otherwise a click toggles it closed.\n if (!keyboard && current.by === 'hover') commit({ id, by: 'click' })\n else commit(CLOSED)\n return\n }\n const touch = lastPointerType.current === 'touch' || lastPointerType.current === 'pen'\n commit({ id, by: keyboard ? 'keyboard' : touch ? 'touch' : 'click' })\n },\n onKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key !== 'ArrowDown') return\n event.preventDefault()\n clearTimers()\n if (stateRef.current.id === id) {\n panelLinks(id)[0]?.focus()\n return\n }\n focusFirstOf.current = id\n commit({ id, by: 'keyboard' })\n },\n })\n\n const panelHandlers = (id: string) => ({\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => {\n const links = panelLinks(id)\n const index = links.indexOf(document.activeElement as HTMLAnchorElement)\n if (index === -1) return\n let next = -1\n if (event.key === 'ArrowDown') next = Math.min(links.length - 1, index + 1)\n else if (event.key === 'ArrowUp') next = index - 1\n else if (event.key === 'Home') next = 0\n else if (event.key === 'End') next = links.length - 1\n else return\n event.preventDefault()\n if (next < 0) trigger(id)?.focus()\n else links[next]?.focus()\n },\n // A link in the panel is clicked: close.\n onClick: (event: MouseEvent<HTMLElement>) => {\n if ((event.target as HTMLElement).closest('a[href]')) close()\n },\n })\n\n return { openId: state.id, openedBy: state.by, close, itemHandlers, triggerHandlers, panelHandlers }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F6: when the phone call bar shows. TotalCare's logic, ported: below 1024 px the bar shows once every call path on
|
|
3
|
+
* screen (the hero's own call and quote buttons, marked `data-bl-call-path`) has scrolled out, and hides again while
|
|
4
|
+
* a call path, the page footer or an open dialog (the drawer, the quote dialog) is on screen, while a form field has
|
|
5
|
+
* focus (v2) and while focus is inside the footer or a call path. The hook keeps `--bl-callbar-height` at the bar's height while it shows and `0px` otherwise, which
|
|
6
|
+
* drives `scroll-padding-bottom` (WCAG 2.4.11) and lifts back-to-top above the bar.
|
|
7
|
+
*/
|
|
8
|
+
import { type RefObject } from 'react';
|
|
9
|
+
/**
|
|
10
|
+
* True while the bar should show. `resetKey` (the route) re-collects the call paths and the footer. The first state
|
|
11
|
+
* is seeded from their boxes before paint, so the bar never flashes on load or on a client navigation while the
|
|
12
|
+
* observer's first report is pending.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useStickyCallBar(rootRef: RefObject<HTMLElement | null>, resetKey: unknown): boolean;
|
|
15
|
+
//# sourceMappingURL=use-sticky-call-bar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sticky-call-bar.d.ts","sourceRoot":"","sources":["../../src/navigation/use-sticky-call-bar.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAyCjE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CA4DnG"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* F6: when the phone call bar shows. TotalCare's logic, ported: below 1024 px the bar shows once every call path on
|
|
4
|
+
* screen (the hero's own call and quote buttons, marked `data-bl-call-path`) has scrolled out, and hides again while
|
|
5
|
+
* a call path, the page footer or an open dialog (the drawer, the quote dialog) is on screen, while a form field has
|
|
6
|
+
* focus (v2) and while focus is inside the footer or a call path. The hook keeps `--bl-callbar-height` at the bar's height while it shows and `0px` otherwise, which
|
|
7
|
+
* drives `scroll-padding-bottom` (WCAG 2.4.11) and lifts back-to-top above the bar.
|
|
8
|
+
*/
|
|
9
|
+
import { useLayoutEffect, useState } from 'react';
|
|
10
|
+
import { CALL_BAR_MEDIA_QUERY, CALL_PATH_SELECTOR } from './constants.js';
|
|
11
|
+
const FIELD_SELECTOR = [
|
|
12
|
+
'input:not([type="button"], [type="submit"], [type="reset"], [type="checkbox"], [type="radio"], [type="hidden"])',
|
|
13
|
+
'textarea',
|
|
14
|
+
'select',
|
|
15
|
+
'[contenteditable=""]',
|
|
16
|
+
'[contenteditable="true"]',
|
|
17
|
+
].join(', ');
|
|
18
|
+
function isField(target) {
|
|
19
|
+
return target instanceof Element && target.matches(FIELD_SELECTOR);
|
|
20
|
+
}
|
|
21
|
+
/** The page footer: the shared footer, or a `<footer>` that isn't inside an article, aside, main, nav or section. */
|
|
22
|
+
function isPageFooter(element) {
|
|
23
|
+
return element.matches('[data-bl="footer"]') || !element.parentElement?.closest('article, aside, main, nav, section');
|
|
24
|
+
}
|
|
25
|
+
function pageFooters() {
|
|
26
|
+
return Array.from(document.querySelectorAll('footer, [data-bl="footer"]')).filter(isPageFooter);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Focus inside the footer or a call path hides the bar at once, before the observer reports (fast Tab stops would
|
|
30
|
+
* otherwise sit under the bar, K5).
|
|
31
|
+
*/
|
|
32
|
+
function inHideZone(target) {
|
|
33
|
+
if (!(target instanceof Element))
|
|
34
|
+
return false;
|
|
35
|
+
if (target.closest(CALL_PATH_SELECTOR))
|
|
36
|
+
return true;
|
|
37
|
+
const footer = target.closest('footer, [data-bl="footer"]');
|
|
38
|
+
return footer !== null && isPageFooter(footer);
|
|
39
|
+
}
|
|
40
|
+
/** True when any part of the element's box is inside the viewport. */
|
|
41
|
+
function inViewport(element) {
|
|
42
|
+
const box = element.getBoundingClientRect();
|
|
43
|
+
return box.bottom > 0 && box.right > 0 && box.top < window.innerHeight && box.left < window.innerWidth;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* True while the bar should show. `resetKey` (the route) re-collects the call paths and the footer. The first state
|
|
47
|
+
* is seeded from their boxes before paint, so the bar never flashes on load or on a client navigation while the
|
|
48
|
+
* observer's first report is pending.
|
|
49
|
+
*/
|
|
50
|
+
export function useStickyCallBar(rootRef, resetKey) {
|
|
51
|
+
const [shown, setShown] = useState(false);
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
const root = rootRef.current;
|
|
54
|
+
if (!root)
|
|
55
|
+
return;
|
|
56
|
+
const html = document.documentElement;
|
|
57
|
+
const narrow = typeof window.matchMedia === 'function' ? window.matchMedia(CALL_BAR_MEDIA_QUERY) : null;
|
|
58
|
+
const onScreen = new Set();
|
|
59
|
+
let fieldFocused = isField(document.activeElement);
|
|
60
|
+
let focusHidden = inHideZone(document.activeElement);
|
|
61
|
+
const update = () => {
|
|
62
|
+
const next = (narrow?.matches ?? false) && onScreen.size === 0 && !fieldFocused && !focusHidden && !document.querySelector('dialog[open]');
|
|
63
|
+
setShown(next);
|
|
64
|
+
html.style.setProperty('--bl-callbar-height', next ? `${Math.round(root.getBoundingClientRect().height)}px` : '0px');
|
|
65
|
+
};
|
|
66
|
+
let intersections;
|
|
67
|
+
if (typeof IntersectionObserver === 'function') {
|
|
68
|
+
intersections = new IntersectionObserver((entries) => {
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.isIntersecting)
|
|
71
|
+
onScreen.add(entry.target);
|
|
72
|
+
else
|
|
73
|
+
onScreen.delete(entry.target);
|
|
74
|
+
}
|
|
75
|
+
update();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const target of [...document.querySelectorAll(CALL_PATH_SELECTOR), ...pageFooters()]) {
|
|
79
|
+
if (inViewport(target))
|
|
80
|
+
onScreen.add(target);
|
|
81
|
+
intersections?.observe(target);
|
|
82
|
+
}
|
|
83
|
+
const dialogs = new MutationObserver(update);
|
|
84
|
+
dialogs.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['open'] });
|
|
85
|
+
const onFocusIn = (event) => {
|
|
86
|
+
fieldFocused = isField(event.target);
|
|
87
|
+
focusHidden = inHideZone(event.target);
|
|
88
|
+
update();
|
|
89
|
+
};
|
|
90
|
+
const onFocusOut = (event) => {
|
|
91
|
+
fieldFocused = isField(event.relatedTarget);
|
|
92
|
+
focusHidden = inHideZone(event.relatedTarget);
|
|
93
|
+
update();
|
|
94
|
+
};
|
|
95
|
+
document.addEventListener('focusin', onFocusIn);
|
|
96
|
+
document.addEventListener('focusout', onFocusOut);
|
|
97
|
+
narrow?.addEventListener('change', update);
|
|
98
|
+
update();
|
|
99
|
+
return () => {
|
|
100
|
+
intersections?.disconnect();
|
|
101
|
+
dialogs.disconnect();
|
|
102
|
+
document.removeEventListener('focusin', onFocusIn);
|
|
103
|
+
document.removeEventListener('focusout', onFocusOut);
|
|
104
|
+
narrow?.removeEventListener('change', update);
|
|
105
|
+
html.style.removeProperty('--bl-callbar-height');
|
|
106
|
+
};
|
|
107
|
+
}, [rootRef, resetKey]);
|
|
108
|
+
return shown;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=use-sticky-call-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sticky-call-bar.js","sourceRoot":"","sources":["../../src/navigation/use-sticky-call-bar.ts"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * F6: when the phone call bar shows. TotalCare's logic, ported: below 1024 px the bar shows once every call path on\n * screen (the hero's own call and quote buttons, marked `data-bl-call-path`) has scrolled out, and hides again while\n * a call path, the page footer or an open dialog (the drawer, the quote dialog) is on screen, while a form field has\n * focus (v2) and while focus is inside the footer or a call path. The hook keeps `--bl-callbar-height` at the bar's height while it shows and `0px` otherwise, which\n * drives `scroll-padding-bottom` (WCAG 2.4.11) and lifts back-to-top above the bar.\n */\nimport { useLayoutEffect, useState, type RefObject } from 'react'\nimport { CALL_BAR_MEDIA_QUERY, CALL_PATH_SELECTOR } from './constants.js'\n\nconst FIELD_SELECTOR = [\n 'input:not([type=\"button\"], [type=\"submit\"], [type=\"reset\"], [type=\"checkbox\"], [type=\"radio\"], [type=\"hidden\"])',\n 'textarea',\n 'select',\n '[contenteditable=\"\"]',\n '[contenteditable=\"true\"]',\n].join(', ')\n\nfunction isField(target: EventTarget | null): boolean {\n return target instanceof Element && target.matches(FIELD_SELECTOR)\n}\n\n/** The page footer: the shared footer, or a `<footer>` that isn't inside an article, aside, main, nav or section. */\nfunction isPageFooter(element: Element): boolean {\n return element.matches('[data-bl=\"footer\"]') || !element.parentElement?.closest('article, aside, main, nav, section')\n}\n\nfunction pageFooters(): Element[] {\n return Array.from(document.querySelectorAll('footer, [data-bl=\"footer\"]')).filter(isPageFooter)\n}\n\n/**\n * Focus inside the footer or a call path hides the bar at once, before the observer reports (fast Tab stops would\n * otherwise sit under the bar, K5).\n */\nfunction inHideZone(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false\n if (target.closest(CALL_PATH_SELECTOR)) return true\n const footer = target.closest('footer, [data-bl=\"footer\"]')\n return footer !== null && isPageFooter(footer)\n}\n\n/** True when any part of the element's box is inside the viewport. */\nfunction inViewport(element: Element): boolean {\n const box = element.getBoundingClientRect()\n return box.bottom > 0 && box.right > 0 && box.top < window.innerHeight && box.left < window.innerWidth\n}\n\n/**\n * True while the bar should show. `resetKey` (the route) re-collects the call paths and the footer. The first state\n * is seeded from their boxes before paint, so the bar never flashes on load or on a client navigation while the\n * observer's first report is pending.\n */\nexport function useStickyCallBar(rootRef: RefObject<HTMLElement | null>, resetKey: unknown): boolean {\n const [shown, setShown] = useState(false)\n\n useLayoutEffect(() => {\n const root = rootRef.current\n if (!root) return\n const html = document.documentElement\n const narrow = typeof window.matchMedia === 'function' ? window.matchMedia(CALL_BAR_MEDIA_QUERY) : null\n const onScreen = new Set<Element>()\n let fieldFocused = isField(document.activeElement)\n let focusHidden = inHideZone(document.activeElement)\n\n const update = () => {\n const next = (narrow?.matches ?? false) && onScreen.size === 0 && !fieldFocused && !focusHidden && !document.querySelector('dialog[open]')\n setShown(next)\n html.style.setProperty('--bl-callbar-height', next ? `${Math.round(root.getBoundingClientRect().height)}px` : '0px')\n }\n\n let intersections: IntersectionObserver | undefined\n if (typeof IntersectionObserver === 'function') {\n intersections = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) onScreen.add(entry.target)\n else onScreen.delete(entry.target)\n }\n update()\n })\n }\n for (const target of [...document.querySelectorAll(CALL_PATH_SELECTOR), ...pageFooters()]) {\n if (inViewport(target)) onScreen.add(target)\n intersections?.observe(target)\n }\n const dialogs = new MutationObserver(update)\n dialogs.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['open'] })\n const onFocusIn = (event: FocusEvent) => {\n fieldFocused = isField(event.target)\n focusHidden = inHideZone(event.target)\n update()\n }\n const onFocusOut = (event: FocusEvent) => {\n fieldFocused = isField(event.relatedTarget)\n focusHidden = inHideZone(event.relatedTarget)\n update()\n }\n document.addEventListener('focusin', onFocusIn)\n document.addEventListener('focusout', onFocusOut)\n narrow?.addEventListener('change', update)\n update()\n\n return () => {\n intersections?.disconnect()\n dialogs.disconnect()\n document.removeEventListener('focusin', onFocusIn)\n document.removeEventListener('focusout', onFocusOut)\n narrow?.removeEventListener('change', update)\n html.style.removeProperty('--bl-callbar-height')\n }\n }, [rootRef, resetKey])\n\n return shown\n}\n"]}
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
import type { ButtonLinkSkin, PhoneLinkSkin, TextLinkSkin } from './primitives/links.js';
|
|
7
7
|
import type { QuoteButtonSkin } from './primitives/quote-button.js';
|
|
8
8
|
import type { ContainerSkin, SectionHeadingSkin } from './primitives/layout.js';
|
|
9
|
+
import type { BackToTopSkin } from './shell/back-to-top.js';
|
|
10
|
+
import type { MobileCallBarSkin } from './shell/mobile-call-bar.js';
|
|
11
|
+
import type { MobileDrawerSkin } from './shell/mobile-drawer.js';
|
|
12
|
+
import type { SiteHeaderSkin } from './shell/site-header.js';
|
|
13
|
+
import type { UtilityBarSkin } from './shell/utility-bar.js';
|
|
14
|
+
import type { DialogSkin } from './form/dialog.js';
|
|
15
|
+
import type { FieldSkin } from './form/field.js';
|
|
16
|
+
import type { PhotoUploadSkin } from './form/photo-upload.js';
|
|
17
|
+
import type { QuoteDialogSkin } from './form/quote-dialog.js';
|
|
18
|
+
import type { QuoteFormSkin } from './form/quote-form.js';
|
|
19
|
+
import type { ThankYouSkin } from './form/thank-you.js';
|
|
9
20
|
export interface BrandlabSkin {
|
|
10
21
|
readonly buttonLink?: ButtonLinkSkin;
|
|
11
22
|
readonly textLink?: TextLinkSkin;
|
|
@@ -13,5 +24,16 @@ export interface BrandlabSkin {
|
|
|
13
24
|
readonly quoteButton?: QuoteButtonSkin;
|
|
14
25
|
readonly container?: ContainerSkin;
|
|
15
26
|
readonly sectionHeading?: SectionHeadingSkin;
|
|
27
|
+
readonly siteHeader?: SiteHeaderSkin;
|
|
28
|
+
readonly mobileDrawer?: MobileDrawerSkin;
|
|
29
|
+
readonly utilityBar?: UtilityBarSkin;
|
|
30
|
+
readonly mobileCallBar?: MobileCallBarSkin;
|
|
31
|
+
readonly backToTop?: BackToTopSkin;
|
|
32
|
+
readonly quoteForm?: QuoteFormSkin;
|
|
33
|
+
readonly quoteDialog?: QuoteDialogSkin;
|
|
34
|
+
readonly photoUpload?: PhotoUploadSkin;
|
|
35
|
+
readonly dialog?: DialogSkin;
|
|
36
|
+
readonly field?: FieldSkin;
|
|
37
|
+
readonly thankYou?: ThankYouSkin;
|
|
16
38
|
}
|
|
17
39
|
//# sourceMappingURL=brandlab-skin.d.ts.map
|
|
@@ -1 +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;
|
|
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;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,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;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,gBAAgB,CAAA;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,cAAc,CAAA;IACpC,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAA;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAElC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAA;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,eAAe,CAAA;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAA;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CACjC"}
|
|
@@ -1 +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"]}
|
|
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'\nimport type { BackToTopSkin } from './shell/back-to-top.js'\nimport type { MobileCallBarSkin } from './shell/mobile-call-bar.js'\nimport type { MobileDrawerSkin } from './shell/mobile-drawer.js'\nimport type { SiteHeaderSkin } from './shell/site-header.js'\nimport type { UtilityBarSkin } from './shell/utility-bar.js'\nimport type { DialogSkin } from './form/dialog.js'\nimport type { FieldSkin } from './form/field.js'\nimport type { PhotoUploadSkin } from './form/photo-upload.js'\nimport type { QuoteDialogSkin } from './form/quote-dialog.js'\nimport type { QuoteFormSkin } from './form/quote-form.js'\nimport type { ThankYouSkin } from './form/thank-you.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 readonly siteHeader?: SiteHeaderSkin\n readonly mobileDrawer?: MobileDrawerSkin\n readonly utilityBar?: UtilityBarSkin\n readonly mobileCallBar?: MobileCallBarSkin\n readonly backToTop?: BackToTopSkin\n // form (3.10.0)\n readonly quoteForm?: QuoteFormSkin\n readonly quoteDialog?: QuoteDialogSkin\n readonly photoUpload?: PhotoUploadSkin\n readonly dialog?: DialogSkin\n readonly field?: FieldSkin\n readonly thankYou?: ThankYouSkin\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/copy.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,GAAG,MAAM,CAEpG"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Fills `{name}` tokens in a site's copy template, for example `"{count} of {max}"`. Unknown tokens stay visible. */
|
|
2
|
+
export function fillCopy(template, values) {
|
|
3
|
+
return template.replace(/\{([a-zA-Z]+)\}/g, (token, key) => (key in values ? String(values[key]) : token));
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"copy.js","sourceRoot":"","sources":["../../../src/ui-kit/form/copy.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,MAAM,UAAU,QAAQ,CAAC,QAAgB,EAAE,MAAiD;IAC1F,OAAO,QAAQ,CAAC,OAAO,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;AACpH,CAAC","sourcesContent":["/** Fills `{name}` tokens in a site's copy template, for example `\"{count} of {max}\"`. Unknown tokens stay visible. */\nexport function fillCopy(template: string, values: Readonly<Record<string, string | number>>): string {\n return template.replace(/\\{([a-zA-Z]+)\\}/g, (token, key: string) => (key in values ? String(values[key]) : token))\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Dialog`: a modal dialog over `HeadlessDialog` (native `<dialog>` with `showModal()`, so the page behind is inert).
|
|
3
|
+
* Its H2 title names it and takes focus when it opens, so a phone keyboard doesn't jump up; Escape and the close
|
|
4
|
+
* button close it and focus returns to the control that opened it. The page doesn't scroll behind it (structure.css).
|
|
5
|
+
*/
|
|
6
|
+
import { type ReactNode, type RefObject } from 'react';
|
|
7
|
+
import { type SkinEntry } from '../skin.js';
|
|
8
|
+
export type DialogSlot = 'root' | 'header' | 'title' | 'close' | 'body';
|
|
9
|
+
export type DialogSkin = SkinEntry<DialogSlot>;
|
|
10
|
+
export interface DialogProps extends DialogSkin {
|
|
11
|
+
/** The dialog's id; its title is `${id}-title`. */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly open: boolean;
|
|
14
|
+
readonly onClose: () => void;
|
|
15
|
+
/** The H2 that names the dialog, for example "Get a free quote". */
|
|
16
|
+
readonly title: string;
|
|
17
|
+
/** The close button's accessible name, for example "Close". */
|
|
18
|
+
readonly closeLabel: string;
|
|
19
|
+
/** Where focus returns on close; by default the element focused when it opened. */
|
|
20
|
+
readonly returnFocusRef?: RefObject<HTMLElement | null> | undefined;
|
|
21
|
+
readonly children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export declare function Dialog({ id, open, onClose, title, closeLabel, returnFocusRef, classes, children }: DialogProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
//# sourceMappingURL=dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/dialog.tsx"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,OAAO,EAAU,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG9D,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEjD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;AACvE,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAA;AAE9C,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,mDAAmD;IACnD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAA;IAC5B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,SAAS,CAAA;IACnE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,WAAW,2CA2B9G"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `Dialog`: a modal dialog over `HeadlessDialog` (native `<dialog>` with `showModal()`, so the page behind is inert).
|
|
5
|
+
* Its H2 title names it and takes focus when it opens, so a phone keyboard doesn't jump up; Escape and the close
|
|
6
|
+
* button close it and focus returns to the control that opened it. The page doesn't scroll behind it (structure.css).
|
|
7
|
+
*/
|
|
8
|
+
import { useRef } from 'react';
|
|
9
|
+
import { HeadlessDialog } from '../../ui-client.js';
|
|
10
|
+
import { Icon } from '../icons.js';
|
|
11
|
+
import { slot } from '../skin.js';
|
|
12
|
+
export function Dialog({ id, open, onClose, title, closeLabel, returnFocusRef, classes, children }) {
|
|
13
|
+
const titleRef = useRef(null);
|
|
14
|
+
const titleId = `${id}-title`;
|
|
15
|
+
return (_jsxs(HeadlessDialog, { id: id, open: open, onClose: onClose, labelledBy: titleId, initialFocusRef: titleRef, ...(returnFocusRef ? { returnFocusRef } : {}), "data-bl": "dialog", "data-state": open ? 'open' : 'closed', className: classes?.root, children: [_jsxs("div", { ...slot(classes, 'header'), children: [_jsx("h2", { ...slot(classes, 'title'), id: titleId, ref: titleRef, tabIndex: -1, children: title }), _jsxs("button", { ...slot(classes, 'close'), type: "button", onClick: onClose, children: [_jsx(Icon, { name: "close" }), _jsx("span", { "data-bl-visually-hidden": "", children: closeLabel })] })] }), _jsx("div", { ...slot(classes, 'body'), children: children })] }));
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=dialog.js.map
|