@naeil/ui 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
  var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
4
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
3
5
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
7
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
@@ -15,9 +17,12 @@ var __spreadValues = (a, b) => {
15
17
  }
16
18
  return a;
17
19
  };
20
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
18
21
  import { jsx, jsxs } from "react/jsx-runtime";
19
22
  import { UserRound } from "lucide-react";
23
+ import { useContext, useRef } from "react";
20
24
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem } from "./ui/dropdown-menu.js";
25
+ import { NavigationSheetAction } from "./ui/navigation.js";
21
26
  const labels = {
22
27
  en: { menu: "Account menu", account: "Account", login: "Log in", logout: "Log out", busy: "Logging out\u2026" },
23
28
  ko: { menu: "\uACC4\uC815 \uBA54\uB274", account: "\uACC4\uC815", login: "\uB85C\uADF8\uC778", logout: "\uB85C\uADF8\uC544\uC6C3", busy: "\uB85C\uADF8\uC544\uC6C3 \uC911\u2026" },
@@ -26,10 +31,18 @@ const labels = {
26
31
  const NativeLink = (props) => /* @__PURE__ */ jsx("a", __spreadValues({}, props));
27
32
  function NaeilHeaderAccountControl({ locale, account, Link = NativeLink }) {
28
33
  const text = labels[locale];
34
+ const handoff = useContext(NavigationSheetAction);
35
+ const pendingAction = useRef(void 0);
29
36
  if (account.status === "signed-out") {
30
37
  return /* @__PURE__ */ jsx(Link, { href: account.loginHref, className: "ui-navigation-link ui-focus naeil-header-login", "data-naeil-account-state": "signed-out", children: text.login });
31
38
  }
32
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
39
+ return /* @__PURE__ */ jsxs(DropdownMenu, { onOpenChange: (open) => {
40
+ var _a;
41
+ if (open) {
42
+ (_a = pendingAction.current) == null ? void 0 : _a.cancel();
43
+ pendingAction.current = void 0;
44
+ }
45
+ }, children: [
33
46
  /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
34
47
  "button",
35
48
  {
@@ -42,24 +55,46 @@ function NaeilHeaderAccountControl({ locale, account, Link = NativeLink }) {
42
55
  children: /* @__PURE__ */ jsx("span", { className: "naeil-header-account-avatar", "aria-hidden": "true", children: /* @__PURE__ */ jsx(UserRound, {}) })
43
56
  }
44
57
  ) }),
45
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56 max-w-[calc(100vw-2rem)]", "data-naeil-account-menu": true, children: [
46
- /* @__PURE__ */ jsxs(DropdownMenuLabel, { className: "flex min-w-0 flex-col font-normal", children: [
47
- /* @__PURE__ */ jsx("span", { className: "truncate text-base font-medium", children: account.displayName || text.account }),
48
- account.email && /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: account.email })
49
- ] }),
50
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
51
- /* @__PURE__ */ jsx(
52
- DropdownMenuItem,
53
- {
54
- className: "min-h-11",
55
- disabled: account.busy,
56
- onSelect: () => {
57
- if (!account.busy) account.onLogout();
58
- },
59
- children: account.busy ? text.busy : text.logout
60
- }
61
- )
62
- ] })
58
+ /* @__PURE__ */ jsxs(
59
+ DropdownMenuContent,
60
+ {
61
+ align: "end",
62
+ className: "w-56 max-w-[calc(100vw-2rem)]",
63
+ "data-naeil-account-menu": true,
64
+ "data-naeil-chrome": "",
65
+ lang: locale,
66
+ onCloseAutoFocus: (event) => {
67
+ const flow = pendingAction.current;
68
+ if (!flow || event.target !== flow.menu) return;
69
+ event.preventDefault();
70
+ flow.menu = null;
71
+ flow.menuClosed();
72
+ },
73
+ children: [
74
+ /* @__PURE__ */ jsxs(DropdownMenuLabel, { className: "flex min-w-0 flex-col font-normal", children: [
75
+ /* @__PURE__ */ jsx("span", { className: "truncate text-base font-medium", children: account.displayName || text.account }),
76
+ account.email && /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: account.email })
77
+ ] }),
78
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
79
+ /* @__PURE__ */ jsx(
80
+ DropdownMenuItem,
81
+ {
82
+ className: "min-h-11",
83
+ disabled: account.busy,
84
+ onSelect: (event) => {
85
+ if (account.busy) return;
86
+ if (handoff) {
87
+ const menu = event.target.closest("[data-naeil-account-menu]");
88
+ if (!(menu == null ? void 0 : menu.isConnected)) return;
89
+ pendingAction.current = __spreadProps(__spreadValues({}, handoff(account.onLogout)), { menu });
90
+ } else account.onLogout();
91
+ },
92
+ children: account.busy ? text.busy : text.logout
93
+ }
94
+ )
95
+ ]
96
+ }
97
+ )
63
98
  ] });
64
99
  }
65
100
  export {
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { NavigationProps, NavigationLinkProps, NavigationLink, NavigationLinkComponent } from './ui/navigation.js';
3
+ import { NavigationProps, NavigationItem, NavigationLinkProps, NavigationLink, NavigationLinkComponent } from './ui/navigation.js';
4
4
  import { NaeilHeaderLocale } from './naeil-header-content.js';
5
5
  export { NaeilHeaderAccount, NaeilHeaderAccountControl, NaeilHeaderAccountControlProps } from './naeil-header-account.js';
6
6
 
@@ -10,6 +10,12 @@ interface NaeilHeaderProps extends Omit<NavigationProps, "brand" | "items" | "to
10
10
  resolveHref?: (path: string) => string;
11
11
  brandHref?: string;
12
12
  brandSlot?: ReactNode;
13
+ /** Ignored when brandSlot replaces the complete brand content. */
14
+ brandSuffix?: string;
15
+ /** After the brand link, outside it; rendered once at every width. */
16
+ statusSlot?: ReactNode;
17
+ /** Product menu data, after catalog/blog and before flat subNav links. */
18
+ appNavigation?: NavigationItem[];
13
19
  brandLinkProps?: Omit<NavigationLinkProps, "href" | "children">;
14
20
  /** Omit when the application does not support changing its UI language. */
15
21
  onLocaleChange?: (locale: NaeilHeaderLocale) => void;
@@ -48,6 +54,6 @@ declare function NaeilHeaderLocaleControl({ locale, onLocaleChange }: {
48
54
  onLocaleChange: (locale: NaeilHeaderLocale) => void;
49
55
  }): react_jsx_runtime.JSX.Element;
50
56
  /** One complete, opt-in branded composition shared by naeil.dev and applications. */
51
- declare function NaeilHeader({ locale, resolveHref, brandHref, brandSlot, brandLinkProps, brandLinkComponent, onLocaleChange, authSlot, toolbarSlot, projects, projectsExternal, translations, showProjects, showBlog, subNav, currentProjectKey, currentProjectHref, labels, Link, currentHref, onNavigate, className, containerClassName, ...props }: NaeilHeaderProps): react_jsx_runtime.JSX.Element;
57
+ declare function NaeilHeader({ locale, resolveHref, brandHref, brandSlot, brandSuffix, statusSlot, appNavigation, brandLinkProps, brandLinkComponent, onLocaleChange, authSlot, toolbarSlot, projects, projectsExternal, translations, showProjects, showBlog, subNav, currentProjectKey, currentProjectHref, labels, Link, currentHref, onNavigate, className, containerClassName, ...props }: NaeilHeaderProps): react_jsx_runtime.JSX.Element;
52
58
 
53
59
  export { NaeilHeader, NaeilHeaderLocale, NaeilHeaderLocaleControl, type NaeilHeaderProps, NaeilHeaderThemeControl };
@@ -72,7 +72,7 @@ function NaeilHeaderLocaleControl({ locale, onLocaleChange }) {
72
72
  children: /* @__PURE__ */ jsx(SelectValue, { children: locale.toUpperCase() })
73
73
  }
74
74
  ),
75
- /* @__PURE__ */ jsx(SelectContent, { align: "end", children: headerLocales.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, className: "min-h-11", lang: value, children: headerLocaleNames[value] }, value)) })
75
+ /* @__PURE__ */ jsx(SelectContent, { align: "end", "data-naeil-chrome": "", "data-naeil-locale-menu": "", lang: locale, children: headerLocales.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, className: "min-h-11", lang: value, children: headerLocaleNames[value] }, value)) })
76
76
  ] });
77
77
  }
78
78
  const NativeLink = (props) => /* @__PURE__ */ jsx("a", __spreadValues({}, props));
@@ -82,6 +82,9 @@ function NaeilHeader(_a) {
82
82
  resolveHref = (path) => `https://naeil.dev/${locale}${path === "/" ? "" : path}`,
83
83
  brandHref,
84
84
  brandSlot,
85
+ brandSuffix,
86
+ statusSlot,
87
+ appNavigation,
85
88
  brandLinkProps,
86
89
  brandLinkComponent,
87
90
  onLocaleChange,
@@ -106,6 +109,9 @@ function NaeilHeader(_a) {
106
109
  "resolveHref",
107
110
  "brandHref",
108
111
  "brandSlot",
112
+ "brandSuffix",
113
+ "statusSlot",
114
+ "appNavigation",
109
115
  "brandLinkProps",
110
116
  "brandLinkComponent",
111
117
  "onLocaleChange",
@@ -148,41 +154,61 @@ function NaeilHeader(_a) {
148
154
  const brandDestination = brandHref != null ? brandHref : resolveHref("/");
149
155
  const BrandLink = brandLinkComponent != null ? brandLinkComponent : Link;
150
156
  const _a2 = brandLinkProps != null ? brandLinkProps : {}, { onClick: onBrandClick } = _a2, brandProps = __objRest(_a2, ["onClick"]);
151
- const toolbar = /* @__PURE__ */ jsx("div", { "data-app-toolbar": true, className: "naeil-header-controls", children: toolbarSlot !== void 0 ? toolbarSlot : /* @__PURE__ */ jsxs(Fragment, { children: [
152
- /* @__PURE__ */ jsx(NaeilHeaderThemeControl, { locale }),
153
- onLocaleChange && /* @__PURE__ */ jsx(NaeilHeaderLocaleControl, { locale, onLocaleChange }),
154
- authSlot && /* @__PURE__ */ jsxs(Fragment, { children: [
155
- /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "naeil-header-separator" }),
156
- authSlot
157
- ] })
158
- ] }) });
157
+ const toolbar = /* @__PURE__ */ jsx(
158
+ "div",
159
+ {
160
+ "data-app-toolbar": true,
161
+ className: "naeil-header-controls",
162
+ "data-naeil-chrome": toolbarSlot === void 0 ? "" : void 0,
163
+ lang: toolbarSlot === void 0 ? locale : void 0,
164
+ children: toolbarSlot !== void 0 ? toolbarSlot : /* @__PURE__ */ jsxs(Fragment, { children: [
165
+ /* @__PURE__ */ jsx(NaeilHeaderThemeControl, { locale }),
166
+ onLocaleChange && /* @__PURE__ */ jsx(NaeilHeaderLocaleControl, { locale, onLocaleChange }),
167
+ authSlot && /* @__PURE__ */ jsxs(Fragment, { children: [
168
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "naeil-header-separator" }),
169
+ authSlot
170
+ ] })
171
+ ] })
172
+ }
173
+ );
174
+ const logo = /* @__PURE__ */ jsx(Logo, { variant: "horizontal", size: 28, className: "block", label: null });
175
+ const brand = /* @__PURE__ */ jsx(
176
+ BrandLink,
177
+ __spreadProps(__spreadValues({
178
+ href: brandDestination,
179
+ "data-app-brand": true,
180
+ "aria-label": brandSlot == null ? ".naeil" : void 0
181
+ }, brandProps), {
182
+ onClick: (event) => {
183
+ onBrandClick == null ? void 0 : onBrandClick(event);
184
+ onNavigate == null ? void 0 : onNavigate(event, { id: "brand", href: brandDestination, label: ".naeil" });
185
+ },
186
+ children: brandSlot != null ? brandSlot : brandSuffix ? /* @__PURE__ */ jsxs(Fragment, { children: [
187
+ logo,
188
+ /* @__PURE__ */ jsx("span", { className: "naeil-header-brand-suffix", children: brandSuffix })
189
+ ] }) : logo
190
+ })
191
+ );
159
192
  return /* @__PURE__ */ jsx(
160
193
  Navigation,
161
194
  __spreadProps(__spreadValues({}, props), {
162
195
  "data-global-header": true,
163
196
  "data-naeil-header": true,
197
+ "data-naeil-chrome": "",
198
+ lang: locale,
164
199
  "data-naeil-ui-version": uiRelease.version,
165
200
  "data-naeil-ui-contract": uiRelease.contractVersion,
166
201
  Link,
167
202
  currentHref,
168
203
  onNavigate,
169
- brand: /* @__PURE__ */ jsx(
170
- BrandLink,
171
- __spreadProps(__spreadValues({
172
- href: brandDestination,
173
- "data-app-brand": true,
174
- "aria-label": brandSlot == null ? ".naeil" : void 0
175
- }, brandProps), {
176
- onClick: (event) => {
177
- onBrandClick == null ? void 0 : onBrandClick(event);
178
- onNavigate == null ? void 0 : onNavigate(event, { id: "brand", href: brandDestination, label: ".naeil" });
179
- },
180
- children: brandSlot != null ? brandSlot : /* @__PURE__ */ jsx(Logo, { variant: "horizontal", size: 28, className: "block", label: null })
181
- })
182
- ),
204
+ brand: statusSlot != null ? /* @__PURE__ */ jsxs("div", { className: "naeil-header-brand-row", children: [
205
+ brand,
206
+ statusSlot
207
+ ] }) : brand,
183
208
  items: [
184
209
  ...showProjects ? [{ id: "projects", label: (_b2 = translations == null ? void 0 : translations.projects) != null ? _b2 : content.nav.projects, items: projectLinks, columns: projectLinks.length > 1 ? 2 : 1 }] : [],
185
210
  ...showBlog ? [{ id: "blog", label: (_c = translations == null ? void 0 : translations.blog) != null ? _c : content.nav.blog, href: resolveHref("/blog"), external: projectsExternal }] : [],
211
+ ...appNavigation != null ? appNavigation : [],
186
212
  ...(subNav != null ? subNav : []).map((item) => __spreadProps(__spreadValues({}, item), { id: item.href }))
187
213
  ],
188
214
  labels: labels != null ? labels : { navigation: content.nav.navigation, menu: content.nav.menu, close: content.nav.closeMenu },
@@ -38,11 +38,17 @@ interface NavigationProps extends Omit<react.ComponentPropsWithoutRef<"header">,
38
38
  toolbar?: react.ReactNode;
39
39
  mobileToolbar?: react.ReactNode;
40
40
  mobileFooter?: react.ReactNode;
41
+ mobileOpen?: boolean;
42
+ onMobileOpenChange?: (open: boolean) => void;
41
43
  /** Use the same product-owned frame class on header, footer and main. */
42
44
  containerClassName?: string;
43
45
  /** Preserve modified clicks; applications may preventDefault for client routing/guards. */
44
46
  onNavigate?: (event: react.MouseEvent<HTMLAnchorElement>, item: NavigationLink) => void;
45
47
  }
46
- declare function Navigation({ brand, items, labels, Link, currentHref, toolbar, mobileToolbar, mobileFooter, containerClassName, onNavigate, className, ...props }: NavigationProps): react_jsx_runtime.JSX.Element;
48
+ declare const NavigationSheetAction: react.Context<((action: () => void) => {
49
+ menuClosed: () => void;
50
+ cancel: () => void;
51
+ }) | undefined>;
52
+ declare function Navigation({ brand, items, labels, Link, currentHref, toolbar, mobileToolbar, mobileFooter, mobileOpen: controlledOpen, onMobileOpenChange, containerClassName, onNavigate, className, ...props }: NavigationProps): react_jsx_runtime.JSX.Element;
47
53
 
48
- export { Navigation, type NavigationGroup, type NavigationItem, type NavigationLink, type NavigationLinkComponent, type NavigationLinkProps, type NavigationProps };
54
+ export { Navigation, type NavigationGroup, type NavigationItem, type NavigationLink, type NavigationLinkComponent, type NavigationLinkProps, type NavigationProps, NavigationSheetAction };
@@ -40,13 +40,15 @@ const NativeLink = React.forwardRef(
40
40
  return /* @__PURE__ */ jsx("a", __spreadValues({ ref }, props));
41
41
  }
42
42
  );
43
+ const NavigationSheetAction = React.createContext(void 0);
43
44
  function DesktopGroup({
44
45
  item,
45
46
  active,
46
47
  open,
47
48
  anotherGroupOpen,
48
49
  onOpenChange,
49
- children
50
+ children,
51
+ chrome
50
52
  }) {
51
53
  var _a;
52
54
  const hoverOpened = React.useRef(false);
@@ -125,7 +127,7 @@ function DesktopGroup({
125
127
  ),
126
128
  /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
127
129
  Popover.Content,
128
- {
130
+ __spreadProps(__spreadValues({}, chrome), {
129
131
  ref: panel,
130
132
  "data-slot": "navigation-popover",
131
133
  className: "ui-navigation-popover ui-popup",
@@ -151,7 +153,7 @@ function DesktopGroup({
151
153
  event.preventDefault();
152
154
  },
153
155
  children: /* @__PURE__ */ jsx("nav", { "aria-label": item.label, className: "ui-navigation-menu-grid", children })
154
- }
156
+ })
155
157
  ) })
156
158
  ]
157
159
  }
@@ -167,6 +169,8 @@ function Navigation(_a) {
167
169
  toolbar,
168
170
  mobileToolbar,
169
171
  mobileFooter,
172
+ mobileOpen: controlledOpen,
173
+ onMobileOpenChange,
170
174
  containerClassName,
171
175
  onNavigate,
172
176
  className
@@ -179,18 +183,75 @@ function Navigation(_a) {
179
183
  "toolbar",
180
184
  "mobileToolbar",
181
185
  "mobileFooter",
186
+ "mobileOpen",
187
+ "onMobileOpenChange",
182
188
  "containerClassName",
183
189
  "onNavigate",
184
190
  "className"
185
191
  ]);
186
- const [mobileOpen, setMobileOpen] = React.useState(false);
192
+ const [internalOpen, setInternalOpen] = React.useState(false);
193
+ const mobileOpen = controlledOpen != null ? controlledOpen : internalOpen;
194
+ const state = React.useRef({ open: mobileOpen, controlled: controlledOpen !== void 0, onChange: onMobileOpenChange });
195
+ const mounted = React.useRef(false);
196
+ const pendingAction = React.useRef(void 0);
197
+ const cancelAction = React.useCallback(() => {
198
+ var _a2;
199
+ clearTimeout((_a2 = pendingAction.current) == null ? void 0 : _a2.timer);
200
+ pendingAction.current = void 0;
201
+ }, []);
202
+ React.useLayoutEffect(() => {
203
+ const reopening = mobileOpen && !state.current.open;
204
+ state.current = { open: mobileOpen, controlled: controlledOpen !== void 0, onChange: onMobileOpenChange };
205
+ if (reopening) cancelAction();
206
+ });
207
+ React.useLayoutEffect(() => {
208
+ mounted.current = true;
209
+ return () => {
210
+ mounted.current = false;
211
+ cancelAction();
212
+ };
213
+ }, [cancelAction]);
214
+ const setMobileOpen = React.useCallback((next, owner) => {
215
+ var _a2, _b2;
216
+ if (next) cancelAction();
217
+ if (next === state.current.open) return;
218
+ if (pendingAction.current !== owner) cancelAction();
219
+ if (!state.current.controlled) {
220
+ state.current.open = next;
221
+ setInternalOpen(next);
222
+ }
223
+ (_b2 = (_a2 = state.current).onChange) == null ? void 0 : _b2.call(_a2, next);
224
+ }, [cancelAction]);
225
+ const chrome = "data-naeil-chrome" in props ? { "data-naeil-chrome": "", lang: props.lang } : {};
187
226
  const [openGroup, setOpenGroup] = React.useState(null);
188
227
  const trigger = React.useRef(null);
189
228
  const desktop = React.useRef(null);
229
+ const sheetContent = React.useRef(null);
230
+ function settleAction(flow) {
231
+ if (!mounted.current || pendingAction.current !== flow || state.current.open || !flow.menuClosed || !flow.sheetClosed || flow.timer !== void 0) return;
232
+ flow.timer = setTimeout(() => {
233
+ var _a2, _b2;
234
+ flow.timer = void 0;
235
+ if (!mounted.current || pendingAction.current !== flow) return;
236
+ if (state.current.open) {
237
+ cancelAction();
238
+ return;
239
+ }
240
+ if (!flow.menuClosed || !flow.sheetClosed) return;
241
+ const target = [trigger.current, (_b2 = (_a2 = desktop.current) == null ? void 0 : _a2.parentElement) == null ? void 0 : _b2.querySelector(".ui-navigation-toolbar [data-naeil-account-trigger]")].find((element) => (element == null ? void 0 : element.isConnected) && element.getBoundingClientRect().width && element.getBoundingClientRect().height);
242
+ cancelAction();
243
+ if (!target) return;
244
+ target.focus();
245
+ flow.action();
246
+ }, 0);
247
+ }
248
+ const previousHref = React.useRef(currentHref);
190
249
  React.useEffect(() => {
250
+ if (previousHref.current === currentHref) return;
251
+ previousHref.current = currentHref;
191
252
  setMobileOpen(false);
192
253
  setOpenGroup(null);
193
- }, [currentHref]);
254
+ }, [currentHref, setMobileOpen]);
194
255
  React.useEffect(() => {
195
256
  const element = desktop.current;
196
257
  if (!element || typeof ResizeObserver === "undefined") return;
@@ -200,7 +261,7 @@ function Navigation(_a) {
200
261
  });
201
262
  observer.observe(element);
202
263
  return () => observer.disconnect();
203
- }, []);
264
+ }, [setMobileOpen]);
204
265
  const active = (item) => {
205
266
  var _a2;
206
267
  return (_a2 = item.active) != null ? _a2 : !item.external && item.href === currentHref;
@@ -220,8 +281,10 @@ function Navigation(_a) {
220
281
  ),
221
282
  onClick: (event) => {
222
283
  onNavigate == null ? void 0 : onNavigate(event, item);
223
- if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)
224
- close == null ? void 0 : close();
284
+ if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) {
285
+ if (close === "mobile") setMobileOpen(false);
286
+ else if (close === "group") setOpenGroup(null);
287
+ }
225
288
  },
226
289
  children: [
227
290
  item.icon && /* @__PURE__ */ jsx("span", { className: "ui-navigation-icon", "aria-hidden": "true", children: item.icon }),
@@ -256,6 +319,7 @@ function Navigation(_a) {
256
319
  (item) => "items" in item ? /* @__PURE__ */ jsx(
257
320
  DesktopGroup,
258
321
  {
322
+ chrome,
259
323
  item,
260
324
  active: item.items.some(active),
261
325
  open: openGroup === item.id,
@@ -264,7 +328,7 @@ function Navigation(_a) {
264
328
  (current) => open ? item.id : current === item.id ? null : current
265
329
  ),
266
330
  children: item.items.map(
267
- (child) => link(child, () => setOpenGroup(null), true)
331
+ (child) => link(child, "group", true)
268
332
  )
269
333
  },
270
334
  item.id
@@ -293,12 +357,26 @@ function Navigation(_a) {
293
357
  ),
294
358
  /* @__PURE__ */ jsxs(
295
359
  Dialog.Content,
296
- {
360
+ __spreadProps(__spreadValues({}, chrome), {
361
+ ref: (element) => {
362
+ if (element) sheetContent.current = element;
363
+ },
297
364
  "data-slot": "navigation-sheet",
298
365
  className: "ui-navigation-sheet ui-popup",
299
366
  "aria-describedby": void 0,
300
367
  onCloseAutoFocus: (event) => {
301
368
  var _a2, _b2;
369
+ if (controlledOpen !== void 0 && (!mounted.current || state.current.open)) {
370
+ event.preventDefault();
371
+ return;
372
+ }
373
+ const flow = pendingAction.current;
374
+ if (flow && event.target === flow.sheet) {
375
+ event.preventDefault();
376
+ flow.sheetClosed = true;
377
+ settleAction(flow);
378
+ return;
379
+ }
302
380
  if (trigger.current && trigger.current.getBoundingClientRect().width === 0) {
303
381
  event.preventDefault();
304
382
  (_b2 = (_a2 = desktop.current) == null ? void 0 : _a2.querySelector("button, a")) == null ? void 0 : _b2.focus();
@@ -325,16 +403,34 @@ function Navigation(_a) {
325
403
  (item) => "items" in item ? /* @__PURE__ */ jsxs("section", { "aria-label": item.label, children: [
326
404
  /* @__PURE__ */ jsx("h3", { className: "ui-navigation-group-label", children: item.label }),
327
405
  item.items.map(
328
- (child) => link(child, () => setMobileOpen(false), true)
406
+ (child) => link(child, "mobile", true)
329
407
  )
330
- ] }, item.id) : link(item, () => setMobileOpen(false))
408
+ ] }, item.id) : link(item, "mobile")
331
409
  )
332
410
  }
333
411
  ),
334
- (mobileToolbar != null ? mobileToolbar : toolbar) && /* @__PURE__ */ jsx("div", { className: "ui-navigation-mobile-toolbar", children: mobileToolbar != null ? mobileToolbar : toolbar }),
412
+ (mobileToolbar != null ? mobileToolbar : toolbar) && /* @__PURE__ */ jsx("div", { className: "ui-navigation-mobile-toolbar", children: /* @__PURE__ */ jsx(NavigationSheetAction.Provider, { value: controlledOpen === void 0 ? void 0 : (action) => {
413
+ if (!mounted.current) return { menuClosed: () => {
414
+ }, cancel: () => {
415
+ } };
416
+ cancelAction();
417
+ const flow = { action, sheet: sheetContent.current, menuClosed: false, sheetClosed: false };
418
+ pendingAction.current = flow;
419
+ return {
420
+ menuClosed: () => {
421
+ if (!mounted.current || pendingAction.current !== flow) return;
422
+ flow.menuClosed = true;
423
+ setMobileOpen(false, flow);
424
+ settleAction(flow);
425
+ },
426
+ cancel: () => {
427
+ if (pendingAction.current === flow) cancelAction();
428
+ }
429
+ };
430
+ }, children: mobileToolbar != null ? mobileToolbar : toolbar }) }),
335
431
  mobileFooter && /* @__PURE__ */ jsx("div", { className: "ui-navigation-mobile-footer", children: mobileFooter })
336
432
  ]
337
- }
433
+ })
338
434
  )
339
435
  ] })
340
436
  ] })
@@ -343,5 +439,6 @@ function Navigation(_a) {
343
439
  );
344
440
  }
345
441
  export {
346
- Navigation
442
+ Navigation,
443
+ NavigationSheetAction
347
444
  };
@@ -1,5 +1,5 @@
1
1
  declare const uiRelease: {
2
- readonly version: "0.7.2";
2
+ readonly version: "0.8.0";
3
3
  readonly contractVersion: "1";
4
4
  };
5
5
 
@@ -1,4 +1,4 @@
1
- const uiRelease = { version: "0.7.2", contractVersion: "1" };
1
+ const uiRelease = { version: "0.8.0", contractVersion: "1" };
2
2
  export {
3
3
  uiRelease
4
4
  };
@@ -1,6 +1,6 @@
1
1
  # Application frame and shared navigation/footer
2
2
 
3
- This maintained guide covers **published and registry-verified @naeil/ui 0.7.1**, combining canonical footer/release identity and shared account presentation with the published 0.6.0 frame. Install exact `@naeil/ui@0.7.1` from npm; hosted Docs and service activation have separate receipts. See [release identities and status](../package-release.md#publication-and-deployment) and [consumer setup](../design/v2-migration.md) for CSS, Tailwind sources and fonts.
3
+ The published registry baseline is **@naeil/ui 0.7.2**; the new Home profile is an **unpublished 0.8.0 candidate**. Install exact `@naeil/ui@0.7.2` for published consumers; hosted Docs and service activation have separate receipts. See [release identities and status](../package-release.md#publication-and-deployment) and [consumer setup](../design/v2-migration.md) for CSS, Tailwind sources and fonts.
4
4
 
5
5
  ## Choose the shared composition first
6
6
 
@@ -8,7 +8,8 @@ This maintained guide covers **published and registry-verified @naeil/ui 0.7.1**
8
8
  | --- | --- |
9
9
  | The actual naeil.dev brand, project catalog and controls | `NaeilHeader` from `@naeil/ui/naeil-header` |
10
10
  | Owned-service login/account/logout presentation | `NaeilHeaderAccountControl` from `@naeil/ui/naeil-header` in `authSlot` |
11
- | A product's own menu | `Navigation` from `@naeil/ui/ui` |
11
+ | Owned naeil service with product menu data (0.8.0 candidate) | Complete `NaeilHeader`, with `appNavigation`, catalog/blog hidden, optional `brandSuffix` and `statusSlot` |
12
+ | A different product menu without canonical naeil chrome | `Navigation` from `@naeil/ui/ui` |
12
13
  | Owned naeil service footer | `NaeilFooter` from `@naeil/ui/naeil-footer` |
13
14
  | Different product footer or explicit content override | `SiteFooter` from `@naeil/ui/ui` |
14
15
  | An existing Next/next-intl consumer | Keep its `Nav` / `Footer` adapters |
@@ -78,7 +79,9 @@ export function ApplicationLayout({ children, locale, Link, currentHref,
78
79
 
79
80
  `NaeilHeader` is fixed; reserve its 65px height and account for it in sticky areas and focus/anchor scroll margins. Default destinations are official naeil.dev URLs. Preserve routing/unsaved-change guards using the [header contract](naeil-header.md) and [footer interception contract](naeil-footer.md); a Link adapter must handle absolute official and GitHub destinations as well as local routes. Only pass `onLocaleChange` when the application supports its complete language set.
80
81
 
81
- For a different product menu, use the [generic Navigation composition](navigation.md) with the same main/footer structure. Generic Navigation is in document flow by default and needs no fixed-header clearance. The [standalone React example](https://github.com/naeil-dev/naeil-ui/tree/main/examples/react) exercises the installed header, narrow form and SiteFooter together.
82
+ For owned naeil product navigation, keep the whole header and pass menu data through `appNavigation`; see the [Home profile and host conditions](naeil-header.md#host-typography-and-mobile-handoff-080-candidate). `mobileOpen`/`onMobileOpenChange` can coordinate the sheet with a consumer confirmation, while busy state and request locking remain above the header. For a different product menu without canonical chrome, use the [generic Navigation composition](navigation.md) with the same main/footer structure. Generic Navigation is in document flow by default and needs no fixed-header clearance. The [standalone React example](https://github.com/naeil-dev/naeil-ui/tree/main/examples/react) exercises the installed header, narrow form and SiteFooter together.
83
+
84
+ An empty desktop menu preserves B1's limitation: resizing alone leaves the sheet open; Escape/close/overlay/route change dismiss it. Controlled-mobile account callbacks wait for both closing layers and their focus handoff; desktop and uncontrolled-mobile account callbacks retain B1's selection-time behavior. A consumer confirmation owns cancellation focus, including a visible desktop account fallback after resize.
82
85
 
83
86
  ## Deliberate overrides
84
87
 
@@ -119,7 +122,7 @@ Owned-service checks also verify canonical header/footer destinations, rendered
119
122
 
120
123
  A blanket ban on `<header>`, `<nav>` or `<footer>` would also reject valid section landmarks and adapters. Review the root composition and its rendered output instead. This repository checks packed React/Next examples; external applications still need their own upgrades, adapter migrations and CI. Publication and deployment are separate work.
121
124
 
122
- See [canonical footer content and save interception](naeil-footer.md). Version 0.7.1 includes package/contract identity from the unpublished 0.7.0 candidate; registry publication, hosted Docs and per-service operational rollout have separate receipts.
125
+ See [canonical footer content and save interception](naeil-footer.md). Version 0.7.2 includes package/contract identity; registry publication, hosted Docs and per-service operational rollout have separate receipts.
123
126
 
124
127
  ## Shared naeil.dev login (services on `*.naeil.dev`)
125
128