@brandlab-digital/site-next 3.8.0 → 3.9.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 (85) hide show
  1. package/dist/instrumentation-client.d.ts +23 -4
  2. package/dist/instrumentation-client.d.ts.map +1 -1
  3. package/dist/instrumentation-client.js +65 -3
  4. package/dist/instrumentation-client.js.map +1 -1
  5. package/dist/navigation/constants.d.ts +25 -0
  6. package/dist/navigation/constants.d.ts.map +1 -0
  7. package/dist/navigation/constants.js +30 -0
  8. package/dist/navigation/constants.js.map +1 -0
  9. package/dist/navigation/index.d.ts +11 -0
  10. package/dist/navigation/index.d.ts.map +1 -0
  11. package/dist/navigation/index.js +11 -0
  12. package/dist/navigation/index.js.map +1 -0
  13. package/dist/navigation/use-back-to-top.d.ts +5 -0
  14. package/dist/navigation/use-back-to-top.d.ts.map +1 -0
  15. package/dist/navigation/use-back-to-top.js +32 -0
  16. package/dist/navigation/use-back-to-top.js.map +1 -0
  17. package/dist/navigation/use-header-metrics.d.ts +17 -0
  18. package/dist/navigation/use-header-metrics.d.ts.map +1 -0
  19. package/dist/navigation/use-header-metrics.js +52 -0
  20. package/dist/navigation/use-header-metrics.js.map +1 -0
  21. package/dist/navigation/use-navigation-menu.d.ts +39 -0
  22. package/dist/navigation/use-navigation-menu.d.ts.map +1 -0
  23. package/dist/navigation/use-navigation-menu.js +206 -0
  24. package/dist/navigation/use-navigation-menu.js.map +1 -0
  25. package/dist/navigation/use-sticky-call-bar.d.ts +15 -0
  26. package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -0
  27. package/dist/navigation/use-sticky-call-bar.js +110 -0
  28. package/dist/navigation/use-sticky-call-bar.js.map +1 -0
  29. package/dist/ui-kit/brandlab-skin.d.ts +10 -0
  30. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  31. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  32. package/dist/ui-kit/shell/back-to-top-client.d.ts +9 -0
  33. package/dist/ui-kit/shell/back-to-top-client.d.ts.map +1 -0
  34. package/dist/ui-kit/shell/back-to-top-client.js +8 -0
  35. package/dist/ui-kit/shell/back-to-top-client.js.map +1 -0
  36. package/dist/ui-kit/shell/back-to-top.d.ts +13 -0
  37. package/dist/ui-kit/shell/back-to-top.d.ts.map +1 -0
  38. package/dist/ui-kit/shell/back-to-top.js +14 -0
  39. package/dist/ui-kit/shell/back-to-top.js.map +1 -0
  40. package/dist/ui-kit/shell/call-bar-client.d.ts +7 -0
  41. package/dist/ui-kit/shell/call-bar-client.d.ts.map +1 -0
  42. package/dist/ui-kit/shell/call-bar-client.js +13 -0
  43. package/dist/ui-kit/shell/call-bar-client.js.map +1 -0
  44. package/dist/ui-kit/shell/current-link.d.ts +15 -0
  45. package/dist/ui-kit/shell/current-link.d.ts.map +1 -0
  46. package/dist/ui-kit/shell/current-link.js +34 -0
  47. package/dist/ui-kit/shell/current-link.js.map +1 -0
  48. package/dist/ui-kit/shell/drawer-client.d.ts +22 -0
  49. package/dist/ui-kit/shell/drawer-client.d.ts.map +1 -0
  50. package/dist/ui-kit/shell/drawer-client.js +70 -0
  51. package/dist/ui-kit/shell/drawer-client.js.map +1 -0
  52. package/dist/ui-kit/shell/header-client.d.ts +44 -0
  53. package/dist/ui-kit/shell/header-client.d.ts.map +1 -0
  54. package/dist/ui-kit/shell/header-client.js +59 -0
  55. package/dist/ui-kit/shell/header-client.js.map +1 -0
  56. package/dist/ui-kit/shell/index.d.ts +6 -0
  57. package/dist/ui-kit/shell/index.d.ts.map +1 -0
  58. package/dist/ui-kit/shell/index.js +6 -0
  59. package/dist/ui-kit/shell/index.js.map +1 -0
  60. package/dist/ui-kit/shell/mobile-call-bar.d.ts +16 -0
  61. package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -0
  62. package/dist/ui-kit/shell/mobile-call-bar.js +15 -0
  63. package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -0
  64. package/dist/ui-kit/shell/mobile-drawer.d.ts +37 -0
  65. package/dist/ui-kit/shell/mobile-drawer.d.ts.map +1 -0
  66. package/dist/ui-kit/shell/mobile-drawer.js +25 -0
  67. package/dist/ui-kit/shell/mobile-drawer.js.map +1 -0
  68. package/dist/ui-kit/shell/shell-context.d.ts +3 -0
  69. package/dist/ui-kit/shell/shell-context.d.ts.map +1 -0
  70. package/dist/ui-kit/shell/shell-context.js +5 -0
  71. package/dist/ui-kit/shell/shell-context.js.map +1 -0
  72. package/dist/ui-kit/shell/site-header.d.ts +52 -0
  73. package/dist/ui-kit/shell/site-header.d.ts.map +1 -0
  74. package/dist/ui-kit/shell/site-header.js +36 -0
  75. package/dist/ui-kit/shell/site-header.js.map +1 -0
  76. package/dist/ui-kit/shell/social-links.d.ts +8 -0
  77. package/dist/ui-kit/shell/social-links.d.ts.map +1 -0
  78. package/dist/ui-kit/shell/social-links.js +9 -0
  79. package/dist/ui-kit/shell/social-links.js.map +1 -0
  80. package/dist/ui-kit/shell/utility-bar.d.ts +30 -0
  81. package/dist/ui-kit/shell/utility-bar.d.ts.map +1 -0
  82. package/dist/ui-kit/shell/utility-bar.js +15 -0
  83. package/dist/ui-kit/shell/utility-bar.js.map +1 -0
  84. package/dist/ui-kit/structure.css +594 -0
  85. package/package.json +10 -2
@@ -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,11 @@
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';
9
14
  export interface BrandlabSkin {
10
15
  readonly buttonLink?: ButtonLinkSkin;
11
16
  readonly textLink?: TextLinkSkin;
@@ -13,5 +18,10 @@ export interface BrandlabSkin {
13
18
  readonly quoteButton?: QuoteButtonSkin;
14
19
  readonly container?: ContainerSkin;
15
20
  readonly sectionHeading?: SectionHeadingSkin;
21
+ readonly siteHeader?: SiteHeaderSkin;
22
+ readonly mobileDrawer?: MobileDrawerSkin;
23
+ readonly utilityBar?: UtilityBarSkin;
24
+ readonly mobileCallBar?: MobileCallBarSkin;
25
+ readonly backToTop?: BackToTopSkin;
16
26
  }
17
27
  //# 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;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"}
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;AAE5D,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;CACnC"}
@@ -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'\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}\n"]}
@@ -0,0 +1,9 @@
1
+ /** The back-to-top button's client leaf (F7 rules in the hook). */
2
+ import type { ReactNode } from 'react';
3
+ export declare function BackToTopButton({ corner, offset, className, children, }: {
4
+ readonly corner: string;
5
+ readonly offset: string;
6
+ readonly className?: string | undefined;
7
+ readonly children: ReactNode;
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=back-to-top-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-to-top-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top-client.tsx"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,MAAM,EACN,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAiBA"}
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useBackToTop } from '../../navigation/use-back-to-top.js';
4
+ export function BackToTopButton({ corner, offset, className, children, }) {
5
+ const { shown, scrollToTop } = useBackToTop();
6
+ return (_jsx("button", { type: "button", "data-bl": "back-to-top", "data-corner": corner, "data-offset": offset, "data-state": shown ? 'shown' : 'hidden', inert: !shown, "aria-label": "Back to top", className: className, onClick: scrollToTop, children: children }));
7
+ }
8
+ //# sourceMappingURL=back-to-top-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-to-top-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAIZ,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAA;AAElE,MAAM,UAAU,eAAe,CAAC,EAC9B,MAAM,EACN,MAAM,EACN,SAAS,EACT,QAAQ,GAMT;IACC,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,YAAY,EAAE,CAAA;IAC7C,OAAO,CACL,iBACE,IAAI,EAAC,QAAQ,aACL,aAAa,iBACR,MAAM,iBACN,MAAM,gBACP,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EACtC,KAAK,EAAE,CAAC,KAAK,gBACF,aAAa,EACxB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,WAAW,YAEnB,QAAQ,GACF,CACV,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/** The back-to-top button's client leaf (F7 rules in the hook). */\nimport type { ReactNode } from 'react'\nimport { useBackToTop } from '../../navigation/use-back-to-top.js'\n\nexport function BackToTopButton({\n corner,\n offset,\n className,\n children,\n}: {\n readonly corner: string\n readonly offset: string\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n const { shown, scrollToTop } = useBackToTop()\n return (\n <button\n type=\"button\"\n data-bl=\"back-to-top\"\n data-corner={corner}\n data-offset={offset}\n data-state={shown ? 'shown' : 'hidden'}\n inert={!shown}\n aria-label=\"Back to top\"\n className={className}\n onClick={scrollToTop}\n >\n {children}\n </button>\n )\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import { type SkinEntry } from '../skin.js';
2
+ export type BackToTopSlot = 'root' | 'icon';
3
+ export declare const BACK_TO_TOP_OFFSETS: readonly ["small", "medium", "large"];
4
+ export interface BackToTopLayout {
5
+ /** The bottom corner it sits in (default `end`). */
6
+ readonly corner?: 'end' | 'start';
7
+ /** Its distance from the side and from the bottom (or the call bar): 0.75, 1.25 or 2 rem (default `medium`). */
8
+ readonly offset?: (typeof BACK_TO_TOP_OFFSETS)[number];
9
+ }
10
+ export type BackToTopSkin = SkinEntry<BackToTopSlot, never, BackToTopLayout>;
11
+ export type BackToTopProps = BackToTopSkin;
12
+ export declare function BackToTop({ layout, classes }: BackToTopProps): import("react/jsx-runtime").JSX.Element;
13
+ //# sourceMappingURL=back-to-top.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-to-top.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAGjD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAA;AAC3C,eAAO,MAAM,mBAAmB,uCAAwC,CAAA;AACxE,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,OAAO,CAAA;IACjC,gHAAgH;IAChH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;CACvD;AACD,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,EAAE,KAAK,EAAE,eAAe,CAAC,CAAA;AAE5E,MAAM,MAAM,cAAc,GAAG,aAAa,CAAA;AAE1C,wBAAgB,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,cAAc,2CAQ5D"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * F7: back-to-top (server component). A 44 px `<button>` named "Back to top" that appears after one and a half
4
+ * screens, sits above the call bar on phones, scrolls smoothly unless the visitor prefers reduced motion and moves
5
+ * focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.
6
+ */
7
+ import { Icon } from '../icons.js';
8
+ import { slot } from '../skin.js';
9
+ import { BackToTopButton } from './back-to-top-client.js';
10
+ export const BACK_TO_TOP_OFFSETS = ['small', 'medium', 'large'];
11
+ export function BackToTop({ layout, classes }) {
12
+ return (_jsx(BackToTopButton, { corner: layout?.corner ?? 'end', offset: layout?.offset ?? 'medium', className: classes?.root, children: _jsx("span", { ...slot(classes, 'icon'), children: _jsx(Icon, { name: "arrow-up" }) }) }));
13
+ }
14
+ //# sourceMappingURL=back-to-top.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"back-to-top.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/back-to-top.tsx"],"names":[],"mappings":";AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAA;AAWxE,MAAM,UAAU,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAkB;IAC3D,OAAO,CACL,KAAC,eAAe,IAAC,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,YAC5G,kBAAU,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,YAC7B,KAAC,IAAI,IAAC,IAAI,EAAC,UAAU,GAAG,GACnB,GACS,CACnB,CAAA;AACH,CAAC","sourcesContent":["/**\n * F7: back-to-top (server component). A 44 px `<button>` named \"Back to top\" that appears after one and a half\n * screens, sits above the call bar on phones, scrolls smoothly unless the visitor prefers reduced motion and moves\n * focus to `#main`. Never a footer text link. Render it once, after the page's `<main>`.\n */\nimport { Icon } from '../icons.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport { BackToTopButton } from './back-to-top-client.js'\n\nexport type BackToTopSlot = 'root' | 'icon'\nexport const BACK_TO_TOP_OFFSETS = ['small', 'medium', 'large'] as const\nexport interface BackToTopLayout {\n /** The bottom corner it sits in (default `end`). */\n readonly corner?: 'end' | 'start'\n /** Its distance from the side and from the bottom (or the call bar): 0.75, 1.25 or 2 rem (default `medium`). */\n readonly offset?: (typeof BACK_TO_TOP_OFFSETS)[number]\n}\nexport type BackToTopSkin = SkinEntry<BackToTopSlot, never, BackToTopLayout>\n\nexport type BackToTopProps = BackToTopSkin\n\nexport function BackToTop({ layout, classes }: BackToTopProps) {\n return (\n <BackToTopButton corner={layout?.corner ?? 'end'} offset={layout?.offset ?? 'medium'} className={classes?.root}>\n <span {...slot(classes, 'icon')}>\n <Icon name=\"arrow-up\" />\n </span>\n </BackToTopButton>\n )\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export declare function CallBarController({ variant, className, children }: {
3
+ readonly variant: string;
4
+ readonly className?: string | undefined;
5
+ readonly children: ReactNode;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=call-bar-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-bar-client.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAG9C,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAStK"}
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /** The call bar's client leaf: shows and hides the server-rendered Call and quote buttons (F6 rules in the hook). */
4
+ import { usePathname } from 'next/navigation';
5
+ import { useRef } from 'react';
6
+ import { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js';
7
+ export function CallBarController({ variant, className, children }) {
8
+ const ref = useRef(null);
9
+ const pathname = usePathname();
10
+ const shown = useStickyCallBar(ref, pathname);
11
+ return (_jsx("div", { ref: ref, "data-bl": "callbar", "data-variant": variant, "data-state": shown ? 'shown' : 'hidden', inert: !shown, className: className, children: children }));
12
+ }
13
+ //# sourceMappingURL=call-bar-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-bar-client.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/call-bar-client.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,qHAAqH;AACrH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAA;AAE1E,MAAM,UAAU,iBAAiB,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAuG;IACrK,MAAM,GAAG,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IAC7C,OAAO,CACL,cAAK,GAAG,EAAE,GAAG,aAAU,SAAS,kBAAe,OAAO,gBAAc,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,YAChI,QAAQ,GACL,CACP,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/** The call bar's client leaf: shows and hides the server-rendered Call and quote buttons (F6 rules in the hook). */\nimport { usePathname } from 'next/navigation'\nimport { useRef, type ReactNode } from 'react'\nimport { useStickyCallBar } from '../../navigation/use-sticky-call-bar.js'\n\nexport function CallBarController({ variant, className, children }: { readonly variant: string; readonly className?: string | undefined; readonly children: ReactNode }) {\n const ref = useRef<HTMLDivElement>(null)\n const pathname = usePathname()\n const shown = useStickyCallBar(ref, pathname)\n return (\n <div ref={ref} data-bl=\"callbar\" data-variant={variant} data-state={shown ? 'shown' : 'hidden'} inert={!shown} className={className}>\n {children}\n </div>\n )\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ /** True when `href` is the current page. External links are never current. */
3
+ export declare function isCurrentPage(pathname: string, href: string): boolean;
4
+ /** True when the page is one of `paths` or below it (the home path matches only itself). */
5
+ export declare function isCurrentSection(pathname: string, paths: readonly string[]): boolean;
6
+ export declare function CurrentLink({ href, slot, hub, prefetch, className, children, }: {
7
+ readonly href: string;
8
+ readonly slot: string;
9
+ readonly hub?: boolean;
10
+ /** False for links in panels and the drawer: hidden panels count as in view and would prefetch every menu page. */
11
+ readonly prefetch?: boolean | undefined;
12
+ readonly className?: string | undefined;
13
+ readonly children: ReactNode;
14
+ }): import("react/jsx-runtime").JSX.Element;
15
+ //# sourceMappingURL=current-link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"current-link.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAOtC,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,WAG3D;AAED,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,WAO1E;AAED,wBAAgB,WAAW,CAAC,EAC1B,IAAI,EACJ,IAAI,EACJ,GAAW,EACX,QAAQ,EACR,SAAS,EACT,QAAQ,GACT,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,mHAAmH;IACnH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAA;CAC7B,2CAcA"}
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * A menu link that marks itself `aria-current="page"` on its own page. A client leaf only because the root layout
5
+ * can't read the route: it receives one href and its server-rendered label, and renders in the server HTML with the
6
+ * right state.
7
+ */
8
+ import Link from 'next/link';
9
+ import { usePathname } from 'next/navigation';
10
+ function normalisePath(path) {
11
+ const bare = path.split(/[?#]/, 1)[0] ?? '';
12
+ return bare.replace(/\/+$/, '') || '/';
13
+ }
14
+ /** True when `href` is the current page. External links are never current. */
15
+ export function isCurrentPage(pathname, href) {
16
+ if (!href.startsWith('/') || href.startsWith('//'))
17
+ return false;
18
+ return normalisePath(pathname) === normalisePath(href);
19
+ }
20
+ /** True when the page is one of `paths` or below it (the home path matches only itself). */
21
+ export function isCurrentSection(pathname, paths) {
22
+ const current = normalisePath(pathname);
23
+ return paths.some((path) => {
24
+ if (!path.startsWith('/') || path.startsWith('//'))
25
+ return false;
26
+ const section = normalisePath(path);
27
+ return current === section || (section !== '/' && current.startsWith(`${section}/`));
28
+ });
29
+ }
30
+ export function CurrentLink({ href, slot, hub = false, prefetch, className, children, }) {
31
+ const pathname = usePathname();
32
+ return (_jsx(Link, { href: href, prefetch: prefetch, "data-slot": slot, "data-hub": hub ? 'true' : undefined, "aria-current": isCurrentPage(pathname, href) ? 'page' : undefined, className: className, children: children }));
33
+ }
34
+ //# sourceMappingURL=current-link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"current-link.js","sourceRoot":"","sources":["../../../src/ui-kit/shell/current-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;GAIG;AACH,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAG7C,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,GAAG,CAAA;AACxC,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,IAAY;IAC1D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IAChE,OAAO,aAAa,CAAC,QAAQ,CAAC,KAAK,aAAa,CAAC,IAAI,CAAC,CAAA;AACxD,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,KAAwB;IACzE,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAA;IACvC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAChE,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;QACnC,OAAO,OAAO,KAAK,OAAO,IAAI,CAAC,OAAO,KAAK,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAC1B,IAAI,EACJ,IAAI,EACJ,GAAG,GAAG,KAAK,EACX,QAAQ,EACR,SAAS,EACT,QAAQ,GAST;IACC,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,OAAO,CACL,KAAC,IAAI,IACH,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,eACP,IAAI,cACL,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,kBACpB,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAChE,SAAS,EAAE,SAAS,YAEnB,QAAQ,GACJ,CACR,CAAA;AACH,CAAC","sourcesContent":["'use client'\n\n/**\n * A menu link that marks itself `aria-current=\"page\"` on its own page. A client leaf only because the root layout\n * can't read the route: it receives one href and its server-rendered label, and renders in the server HTML with the\n * right state.\n */\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport type { ReactNode } from 'react'\n\nfunction normalisePath(path: string) {\n const bare = path.split(/[?#]/, 1)[0] ?? ''\n return bare.replace(/\\/+$/, '') || '/'\n}\n\n/** True when `href` is the current page. External links are never current. */\nexport function isCurrentPage(pathname: string, href: string) {\n if (!href.startsWith('/') || href.startsWith('//')) return false\n return normalisePath(pathname) === normalisePath(href)\n}\n\n/** True when the page is one of `paths` or below it (the home path matches only itself). */\nexport function isCurrentSection(pathname: string, paths: readonly string[]) {\n const current = normalisePath(pathname)\n return paths.some((path) => {\n if (!path.startsWith('/') || path.startsWith('//')) return false\n const section = normalisePath(path)\n return current === section || (section !== '/' && current.startsWith(`${section}/`))\n })\n}\n\nexport function CurrentLink({\n href,\n slot,\n hub = false,\n prefetch,\n className,\n children,\n}: {\n readonly href: string\n readonly slot: string\n readonly hub?: boolean\n /** False for links in panels and the drawer: hidden panels count as in view and would prefetch every menu page. */\n readonly prefetch?: boolean | undefined\n readonly className?: string | undefined\n readonly children: ReactNode\n}) {\n const pathname = usePathname()\n return (\n <Link\n href={href}\n prefetch={prefetch}\n data-slot={slot}\n data-hub={hub ? 'true' : undefined}\n aria-current={isCurrentPage(pathname, href) ? 'page' : undefined}\n className={className}\n >\n {children}\n </Link>\n )\n}\n"]}