@naeil/ui 0.7.2 → 0.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 (34) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/DESIGN.md +5 -1
  3. package/README.md +6 -6
  4. package/dist/{chunk-7JBLPGZ4.js → chunk-JDHGKM25.js} +1 -1
  5. package/dist/{chunk-QQQQZEPV.js → chunk-KPE4KKYM.js} +111 -14
  6. package/dist/{chunk-MGCCKOGU.js → chunk-XRO3NJAB.js} +102 -44
  7. package/dist/components/index.d.ts +2 -2
  8. package/dist/components/index.js +3 -3
  9. package/dist/components/naeil-footer.d.ts +2 -2
  10. package/dist/components/naeil-footer.js +3 -1
  11. package/dist/components/naeil-header.d.ts +8 -2
  12. package/dist/components/naeil-header.js +3 -3
  13. package/dist/components/ui/index.d.ts +2 -2
  14. package/dist/components/ui/index.js +1 -1
  15. package/dist/lib/ui-release.d.ts +1 -1
  16. package/dist/lib/ui-release.js +1 -1
  17. package/dist/{navigation-CgPeL9tF.d.ts → navigation-D8N1tMTu.d.ts} +4 -2
  18. package/dist/{site-footer-vXDa_6yr.d.ts → site-footer-s113FZgC.d.ts} +1 -1
  19. package/dist/source/components/naeil-footer.js +2 -0
  20. package/dist/source/components/naeil-header-account.js +54 -19
  21. package/dist/source/components/naeil-header.d.ts +8 -2
  22. package/dist/source/components/naeil-header.js +49 -23
  23. package/dist/source/components/ui/navigation.d.ts +8 -2
  24. package/dist/source/components/ui/navigation.js +112 -15
  25. package/dist/source/lib/ui-release.d.ts +1 -1
  26. package/dist/source/lib/ui-release.js +1 -1
  27. package/docs/components/README.md +1 -1
  28. package/docs/components/application-layout.md +28 -7
  29. package/docs/components/common.md +2 -2
  30. package/docs/components/naeil-header.md +39 -5
  31. package/docs/design/v2-migration.md +8 -4
  32. package/docs/package-release.md +25 -4
  33. package/package.json +1 -1
  34. package/src/styles/components.css +130 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # Changelog
2
2
 
3
- ## 0.7.2 — retired project catalog (candidate)
3
+ ## 0.9.0 — page-root scrollbar reservation
4
+
5
+ - Reserve one classic scrollbar gutter at the document root through both package CSS paths; overlay scrollbars reserve no space. The root `data-naeil-scrollbar-gutter="auto"` opt-out disables reservation and shared lock correction together.
6
+ - Remove duplicate Radix body margin/gap compensation under the reserved root without changing scroll prevention, modal focus or cleanup. Preserve all frame, typography and spacing tokens.
7
+ - Document the existing Japanese 320px classic signed-out login toolbar wrap, repair comparison capture ordering and verify real clean React/Next CSS consumers against frozen 0.8.0. See the [adopted acceptance contract](https://github.com/naeil-dev/naeil-ui/blob/434b7d07235316097ca3a55265ee02c1422b0cfd/docs/superpowers/specs/2026-10-29-page-root-scrollbar-gutter.md). Publication, tags, main/site merge and consumer deployments require separate approval.
8
+
9
+ ## 0.8.0 — canonical Home chrome (published 2026-10-28)
10
+
11
+ Published to npm with dist-tag `next`; `latest` remained 0.7.2 at the 2026-10-29 verification. Frozen source/tag: `3f708e27d05212af7f7a5be769f13f871e8d10c7`; archive SHA-256: `6feb5a8cd50265ace584a0855432683b4127119377e08235282eaebd27081084` (139 files). Hosted Docs and service deployments require separate receipts; see the [release policy](docs/package-release.md#publication-and-deployment).
12
+
13
+ - Add `appNavigation`, `brandSuffix` and `statusSlot` for complete owned-service header compositions, plus controlled/observer mobile-sheet state on NaeilHeader and Navigation.
14
+ - Isolate canonical chrome typography from host styles and preserve mobile account/confirmation focus handoffs. Keep existing imports, slots, default catalog and consumer routing/save guards.
15
+ - Retain chrome contract version 1 and verify the Home Workspace/LoginGate profiles against the canonical 0.7.2 baseline. See [NaeilHeader](docs/components/naeil-header.md) for API and compatibility limits.
16
+
17
+ ## 0.7.2 — retired project catalog (published)
18
+
19
+ Published to npm; `latest=0.7.2` at the 2026-10-29 verification. Source/tag: `40a3928e24cd2f0141be3e605eed0a75260938fd`. Registry, hosted Docs and service activation remain separate receipts.
4
20
 
5
21
  - Narrow the canonical `NaeilHeader` project catalog to naeil-ui. Content Collector, PKM, Sustainability Analyzer and Baby Agent are retired from naeil.dev; their site pages move to `src/app/_archive/` and old URLs temporarily redirect to the project list. The project menu uses one column for a single entry. `projects`, `translations` and `currentProjectKey` overrides are unchanged.
6
22
  - Release class `dom`: the standing worker adopts it only through an owner-approved `reviewedMigration` of this exact release; ordinary fix-only patches are unaffected.
package/DESIGN.md CHANGED
@@ -112,9 +112,13 @@ Demonstrate the system in three neutral compositions: a settings form, a selecta
112
112
 
113
113
  On 2026-10-16 the owner approved a shared maximum application frame of **1600px**, with responsive inline gutters **clamp(16px, 2.4vw, 40px)**, after inspecting Naeil Home at mobile and desktop widths. This supersedes the prior 1280px branded-header cap and fixed 24px frame gutters. The frame includes its padding, centers on wider screens and shrinks to available space. Numeric values live in `ui.width-frame` and `ui.frame-gutter`; `.ui-app-frame`, `Navigation`, `SiteFooter`, `NaeilHeader`, and the legacy adapters use those same defaults.
114
114
 
115
+ The page root reserves scrollbar space. Shared full-page frames use `html { scrollbar-gutter: stable; }`, with one scrollbar-side gutter in classic mode and no reserved space in overlay mode. Header, main and footer use the resulting available page width. Changing page length or opening and closing a scroll-locking surface MUST NOT move those frames or their content horizontally. A consumer using a different document scroll policy MUST declare the documented root opt-out and own that policy consistently across routes.
116
+
117
+ The 0.9.0 candidate implements this default once in `components.css`, through either `globals.css` or `theme.css + components.css`. Theme-only/JS-only imports do not set it. `<html data-naeil-scrollbar-gutter="auto">` opts out of both the root reservation and its Radix duplicate-compensation correction before first paint. The default profile is LTR with zero body edge spacing; custom scrollers, body margins or dependency helper classes require consumer integration/opt-out. Unsupported browsers retain existing scroll-lock compensation. See the [adopted frame contract](https://github.com/naeil-dev/naeil-ui/blob/434b7d07235316097ca3a55265ee02c1422b0cfd/docs/superpowers/specs/2026-10-29-page-root-scrollbar-gutter.md) and [consumer override guidance](docs/components/application-layout.md).
118
+
115
119
  The optional `@naeil/ui/naeil-header` composition centralizes the actual naeil.dev brand/catalog/control/toolbar/responsive implementation approved on 2026-10-15. Official decorative image URLs remain references; no new artwork rights or redistribution is implied. See [NaeilHeader](docs/components/naeil-header.md).
116
120
 
117
- Application navigation and footer must use the shared compositions: `NaeilHeader` for the canonical naeil catalog, `Navigation` for different product menus, and `SiteFooter` for both compact and full footers. Existing `Nav`/`Footer` preserve Next compatibility while using that core. Services own menu data, route guards, locale/theme providers and authentication through adapters and slots. Extend the shared API when a reusable behavior is missing instead of copying menu, responsive or footer implementations. See [the application starter and consumer-agent rules](docs/components/application-layout.md).
121
+ Application navigation and footer must use the shared compositions: complete `NaeilHeader` with `NaeilFooter` for owned naeil services, supplying product menu data through `appNavigation`; generic `Navigation`/`SiteFooter` for a different product without canonical naeil chrome. Existing `Nav`/`Footer` preserve Next compatibility while using that core. Services own menu data, route guards, locale/theme providers and authentication through adapters and slots. Extend the shared API when a reusable behavior is missing instead of copying menu, responsive or footer implementations. See [the application starter and consumer-agent rules](docs/components/application-layout.md).
118
122
 
119
123
  The application frame owns navigation placement and outer gutters; each content region owns its purpose-specific width. A narrow settings form inside that frame must not move the shared navigation. For routes using the same frame, keep navigation anchors stable at the same viewport, language and theme, including loading and changing summary counts. A shared Header import alone does not establish this consistency.
120
124
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  React components, semantic design tokens, and shared styles for naeil products. The approved system uses a neutral light/dark palette, Pretendard, 16px controls, comfortable or compact density, purpose-specific content widths, and restrained motion. Radix supplies interaction behavior.
4
4
 
5
- **0.7.1 published and registry-verified — 2026-10-23.** The current published baseline is **0.7.1**. This first published 0.7.x release combines canonical `NaeilFooter`, server-safe `/release` identity and `NaeilHeaderAccountControl`; 0.7.0 remains an unpublished candidate. The frozen package source and `v0.7.1` tag are `32108fd8e1cb1db6dae24a09f3ca72ac9a91f618`. Fresh React/Next registry consumers matched all 139 frozen files and passed types, production builds and browser checks. Maintained guides use separately reviewed source; hosted Docs and standing activation require separate receipts. See the [release policy and receipts](docs/package-release.md#publication-and-deployment).
5
+ **Version 0.9.0 adds the shared page-root scrollbar reservation and scroll-lock correction.** Use an exact version with verified registry publication and archive identity. At the 2026-10-29 verification, the published 0.8.0 archive matched frozen SHA-256 `6feb5a8cd50265ace584a0855432683b4127119377e08235282eaebd27081084` (139 files, source `3f708e27d05212af7f7a5be769f13f871e8d10c7`); tags were `latest=0.7.2`, `next=0.8.0`. Current publication, hosted Docs and per-service deployment have separate receipts in the [release policy](docs/package-release.md#publication-and-deployment).
6
6
 
7
7
  This repository also contains the **naeil.dev brand/example website** and an **optional agent reference workflow**. Choose the path you need:
8
8
 
@@ -15,13 +15,13 @@ This repository also contains the **naeil.dev brand/example website** and an **o
15
15
 
16
16
  ## Use the UI
17
17
 
18
- Use React 19 / ReactDOM 19 and Tailwind 4. UI v2 names the design generation independently of npm versioning. Install the verified registry release:
18
+ Use React 19 / ReactDOM 19 and Tailwind 4. UI v2 names the design generation independently of npm versioning. The installation example targets 0.9.0; run it after verifying that exact version's registry publication:
19
19
 
20
20
  ```sh
21
- pnpm add --save-exact @naeil/ui@0.7.1
21
+ pnpm add --save-exact @naeil/ui@0.9.0
22
22
  ```
23
23
 
24
- The [npm 0.7.1 archive](https://registry.npmjs.org/@naeil/ui/-/ui-0.7.1.tgz) is byte-identical to the frozen release archive. Its bundled guides retain the preparation snapshot; use these maintained instructions for current installation. For local development, `pnpm build:pkg` and `npm pack --ignore-scripts --pack-destination /tmp` produce `/tmp/naeil-ui-0.7.1.tgz`. A fresh pack of maintained source differs from the frozen release archive; use the exact frozen file for release verification. The [0.6.0 release record](https://github.com/naeil-dev/naeil-ui/releases/tag/v0.6.0) retains its archive, publication and deployment evidence.
24
+ The [npm 0.8.0 archive](https://registry.npmjs.org/@naeil/ui/-/ui-0.8.0.tgz) is byte-identical to its frozen release archive. Bundled guides record the preparation snapshot; maintained guides record later publication outcomes. For local development, `pnpm build:pkg` and `npm pack --ignore-scripts --pack-destination /tmp` produce `/tmp/naeil-ui-0.9.0.tgz` from the checked-out source. Use the exact reviewed archive for release verification; later source or guide changes require a new freeze. The [0.6.0 release record](https://github.com/naeil-dev/naeil-ui/releases/tag/v0.6.0) retains its archive, publication and deployment evidence.
25
25
 
26
26
  The preceding [0.5.0 release](https://github.com/naeil-dev/naeil-ui/releases/tag/v0.5.0) was published on 2026-10-15 and introduced the whole shared header. Version 0.6.0 adds the shared 1600px frame; existing applications must upgrade and adopt its layout composition.
27
27
 
@@ -34,11 +34,11 @@ Choose the shared application composition before building a header or footer:
34
34
  | Actual naeil.dev brand, project catalog and controls | `NaeilHeader` from `@naeil/ui/naeil-header` |
35
35
  | Owned-service login, account and logout presentation (0.7.1) | `NaeilHeaderAccountControl` from `@naeil/ui/naeil-header`, passed in `authSlot` |
36
36
  | Owned naeil service footer (0.7.1) | `NaeilFooter` from `@naeil/ui/naeil-footer` |
37
- | A product's own menu | `Navigation` from `@naeil/ui/ui` |
37
+ | A different product without canonical naeil chrome | `Navigation` from `@naeil/ui/ui` |
38
38
  | Different product footer or deliberate content override | `SiteFooter` from `@naeil/ui/ui` |
39
39
  | Existing Next/next-intl integration | Preserve `Nav` / `Footer` compatibility adapters |
40
40
 
41
- Pair `NaeilHeader` and `NaeilFooter` in owned services. Supply application routing, guards, authentication and menu data through adapters and slots. `NaeilHeaderAccountControl` receives session state, a busy flag and the guarded logout callback; keep pending state and its request lock above the header so desktop/mobile copies share them. Keep the shared menu, mobile sheet, account presentation and footer implementation in the package. The [application layout guide](docs/components/application-layout.md) provides a starter and consumer-agent instructions. Header, main and footer share a 1600px maximum frame with `clamp(16px, 2.4vw, 40px)` gutters; use `ui-app-frame` on main and keep 640px reading/settings or 480px short-form sections inside it.
41
+ Pair the complete `NaeilHeader` and `NaeilFooter` in owned services, including product menus via `appNavigation`. Supply application routing, guards, authentication and menu data through adapters and slots. `NaeilHeaderAccountControl` receives session state, a busy flag and the guarded logout callback; keep pending state and its request lock above the header so desktop/mobile copies share them. Keep the shared menu, mobile sheet, account presentation and footer implementation in the package. The [application layout guide](docs/components/application-layout.md) provides a starter and consumer-agent instructions. Header, main and footer share a 1600px maximum frame with `clamp(16px, 2.4vw, 40px)` gutters; use `ui-app-frame` on main and keep 640px reading/settings or 480px short-form sections inside it.
42
42
 
43
43
  Import primitives from the dedicated entrypoint:
44
44
 
@@ -1165,7 +1165,7 @@ function Logo(_a) {
1165
1165
  }
1166
1166
 
1167
1167
  // src/lib/ui-release.ts
1168
- var uiRelease = { version: "0.7.2", contractVersion: "1" };
1168
+ var uiRelease = { version: "0.9.0", contractVersion: "1" };
1169
1169
 
1170
1170
  export {
1171
1171
  Logo,
@@ -343,13 +343,15 @@ var NativeLink = React.forwardRef(
343
343
  return /* @__PURE__ */ jsx4("a", __spreadValues({ ref }, props));
344
344
  }
345
345
  );
346
+ var NavigationSheetAction = React.createContext(void 0);
346
347
  function DesktopGroup({
347
348
  item,
348
349
  active,
349
350
  open,
350
351
  anotherGroupOpen,
351
352
  onOpenChange,
352
- children
353
+ children,
354
+ chrome
353
355
  }) {
354
356
  var _a;
355
357
  const hoverOpened = React.useRef(false);
@@ -428,7 +430,7 @@ function DesktopGroup({
428
430
  ),
429
431
  /* @__PURE__ */ jsx4(Popover.Portal, { children: /* @__PURE__ */ jsx4(
430
432
  Popover.Content,
431
- {
433
+ __spreadProps(__spreadValues({}, chrome), {
432
434
  ref: panel,
433
435
  "data-slot": "navigation-popover",
434
436
  className: "ui-navigation-popover ui-popup",
@@ -454,7 +456,7 @@ function DesktopGroup({
454
456
  event.preventDefault();
455
457
  },
456
458
  children: /* @__PURE__ */ jsx4("nav", { "aria-label": item.label, className: "ui-navigation-menu-grid", children })
457
- }
459
+ })
458
460
  ) })
459
461
  ]
460
462
  }
@@ -470,6 +472,8 @@ function Navigation(_a) {
470
472
  toolbar,
471
473
  mobileToolbar,
472
474
  mobileFooter,
475
+ mobileOpen: controlledOpen,
476
+ onMobileOpenChange,
473
477
  containerClassName,
474
478
  onNavigate,
475
479
  className
@@ -482,18 +486,75 @@ function Navigation(_a) {
482
486
  "toolbar",
483
487
  "mobileToolbar",
484
488
  "mobileFooter",
489
+ "mobileOpen",
490
+ "onMobileOpenChange",
485
491
  "containerClassName",
486
492
  "onNavigate",
487
493
  "className"
488
494
  ]);
489
- const [mobileOpen, setMobileOpen] = React.useState(false);
495
+ const [internalOpen, setInternalOpen] = React.useState(false);
496
+ const mobileOpen = controlledOpen != null ? controlledOpen : internalOpen;
497
+ const state = React.useRef({ open: mobileOpen, controlled: controlledOpen !== void 0, onChange: onMobileOpenChange });
498
+ const mounted = React.useRef(false);
499
+ const pendingAction = React.useRef(void 0);
500
+ const cancelAction = React.useCallback(() => {
501
+ var _a2;
502
+ clearTimeout((_a2 = pendingAction.current) == null ? void 0 : _a2.timer);
503
+ pendingAction.current = void 0;
504
+ }, []);
505
+ React.useLayoutEffect(() => {
506
+ const reopening = mobileOpen && !state.current.open;
507
+ state.current = { open: mobileOpen, controlled: controlledOpen !== void 0, onChange: onMobileOpenChange };
508
+ if (reopening) cancelAction();
509
+ });
510
+ React.useLayoutEffect(() => {
511
+ mounted.current = true;
512
+ return () => {
513
+ mounted.current = false;
514
+ cancelAction();
515
+ };
516
+ }, [cancelAction]);
517
+ const setMobileOpen = React.useCallback((next, owner) => {
518
+ var _a2, _b2;
519
+ if (next) cancelAction();
520
+ if (next === state.current.open) return;
521
+ if (pendingAction.current !== owner) cancelAction();
522
+ if (!state.current.controlled) {
523
+ state.current.open = next;
524
+ setInternalOpen(next);
525
+ }
526
+ (_b2 = (_a2 = state.current).onChange) == null ? void 0 : _b2.call(_a2, next);
527
+ }, [cancelAction]);
528
+ const chrome = "data-naeil-chrome" in props ? { "data-naeil-chrome": "", lang: props.lang } : {};
490
529
  const [openGroup, setOpenGroup] = React.useState(null);
491
530
  const trigger = React.useRef(null);
492
531
  const desktop = React.useRef(null);
532
+ const sheetContent = React.useRef(null);
533
+ function settleAction(flow) {
534
+ if (!mounted.current || pendingAction.current !== flow || state.current.open || !flow.menuClosed || !flow.sheetClosed || flow.timer !== void 0) return;
535
+ flow.timer = setTimeout(() => {
536
+ var _a2, _b2;
537
+ flow.timer = void 0;
538
+ if (!mounted.current || pendingAction.current !== flow) return;
539
+ if (state.current.open) {
540
+ cancelAction();
541
+ return;
542
+ }
543
+ if (!flow.menuClosed || !flow.sheetClosed) return;
544
+ 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);
545
+ cancelAction();
546
+ if (!target) return;
547
+ target.focus();
548
+ flow.action();
549
+ }, 0);
550
+ }
551
+ const previousHref = React.useRef(currentHref);
493
552
  React.useEffect(() => {
553
+ if (previousHref.current === currentHref) return;
554
+ previousHref.current = currentHref;
494
555
  setMobileOpen(false);
495
556
  setOpenGroup(null);
496
- }, [currentHref]);
557
+ }, [currentHref, setMobileOpen]);
497
558
  React.useEffect(() => {
498
559
  const element = desktop.current;
499
560
  if (!element || typeof ResizeObserver === "undefined") return;
@@ -503,7 +564,7 @@ function Navigation(_a) {
503
564
  });
504
565
  observer.observe(element);
505
566
  return () => observer.disconnect();
506
- }, []);
567
+ }, [setMobileOpen]);
507
568
  const active = (item) => {
508
569
  var _a2;
509
570
  return (_a2 = item.active) != null ? _a2 : !item.external && item.href === currentHref;
@@ -523,8 +584,10 @@ function Navigation(_a) {
523
584
  ),
524
585
  onClick: (event) => {
525
586
  onNavigate == null ? void 0 : onNavigate(event, item);
526
- if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)
527
- close == null ? void 0 : close();
587
+ if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey) {
588
+ if (close === "mobile") setMobileOpen(false);
589
+ else if (close === "group") setOpenGroup(null);
590
+ }
528
591
  },
529
592
  children: [
530
593
  item.icon && /* @__PURE__ */ jsx4("span", { className: "ui-navigation-icon", "aria-hidden": "true", children: item.icon }),
@@ -559,6 +622,7 @@ function Navigation(_a) {
559
622
  (item) => "items" in item ? /* @__PURE__ */ jsx4(
560
623
  DesktopGroup,
561
624
  {
625
+ chrome,
562
626
  item,
563
627
  active: item.items.some(active),
564
628
  open: openGroup === item.id,
@@ -567,7 +631,7 @@ function Navigation(_a) {
567
631
  (current) => open ? item.id : current === item.id ? null : current
568
632
  ),
569
633
  children: item.items.map(
570
- (child) => link(child, () => setOpenGroup(null), true)
634
+ (child) => link(child, "group", true)
571
635
  )
572
636
  },
573
637
  item.id
@@ -596,12 +660,26 @@ function Navigation(_a) {
596
660
  ),
597
661
  /* @__PURE__ */ jsxs2(
598
662
  Dialog.Content,
599
- {
663
+ __spreadProps(__spreadValues({}, chrome), {
664
+ ref: (element) => {
665
+ if (element) sheetContent.current = element;
666
+ },
600
667
  "data-slot": "navigation-sheet",
601
668
  className: "ui-navigation-sheet ui-popup",
602
669
  "aria-describedby": void 0,
603
670
  onCloseAutoFocus: (event) => {
604
671
  var _a2, _b2;
672
+ if (controlledOpen !== void 0 && (!mounted.current || state.current.open)) {
673
+ event.preventDefault();
674
+ return;
675
+ }
676
+ const flow = pendingAction.current;
677
+ if (flow && event.target === flow.sheet) {
678
+ event.preventDefault();
679
+ flow.sheetClosed = true;
680
+ settleAction(flow);
681
+ return;
682
+ }
605
683
  if (trigger.current && trigger.current.getBoundingClientRect().width === 0) {
606
684
  event.preventDefault();
607
685
  (_b2 = (_a2 = desktop.current) == null ? void 0 : _a2.querySelector("button, a")) == null ? void 0 : _b2.focus();
@@ -628,16 +706,34 @@ function Navigation(_a) {
628
706
  (item) => "items" in item ? /* @__PURE__ */ jsxs2("section", { "aria-label": item.label, children: [
629
707
  /* @__PURE__ */ jsx4("h3", { className: "ui-navigation-group-label", children: item.label }),
630
708
  item.items.map(
631
- (child) => link(child, () => setMobileOpen(false), true)
709
+ (child) => link(child, "mobile", true)
632
710
  )
633
- ] }, item.id) : link(item, () => setMobileOpen(false))
711
+ ] }, item.id) : link(item, "mobile")
634
712
  )
635
713
  }
636
714
  ),
637
- (mobileToolbar != null ? mobileToolbar : toolbar) && /* @__PURE__ */ jsx4("div", { className: "ui-navigation-mobile-toolbar", children: mobileToolbar != null ? mobileToolbar : toolbar }),
715
+ (mobileToolbar != null ? mobileToolbar : toolbar) && /* @__PURE__ */ jsx4("div", { className: "ui-navigation-mobile-toolbar", children: /* @__PURE__ */ jsx4(NavigationSheetAction.Provider, { value: controlledOpen === void 0 ? void 0 : (action) => {
716
+ if (!mounted.current) return { menuClosed: () => {
717
+ }, cancel: () => {
718
+ } };
719
+ cancelAction();
720
+ const flow = { action, sheet: sheetContent.current, menuClosed: false, sheetClosed: false };
721
+ pendingAction.current = flow;
722
+ return {
723
+ menuClosed: () => {
724
+ if (!mounted.current || pendingAction.current !== flow) return;
725
+ flow.menuClosed = true;
726
+ setMobileOpen(false, flow);
727
+ settleAction(flow);
728
+ },
729
+ cancel: () => {
730
+ if (pendingAction.current === flow) cancelAction();
731
+ }
732
+ };
733
+ }, children: mobileToolbar != null ? mobileToolbar : toolbar }) }),
638
734
  mobileFooter && /* @__PURE__ */ jsx4("div", { className: "ui-navigation-mobile-footer", children: mobileFooter })
639
735
  ]
640
- }
736
+ })
641
737
  )
642
738
  ] })
643
739
  ] })
@@ -894,6 +990,7 @@ export {
894
990
  SelectSeparator,
895
991
  SelectScrollUpButton,
896
992
  SelectScrollDownButton,
993
+ NavigationSheetAction,
897
994
  Navigation,
898
995
  DropdownMenu,
899
996
  DropdownMenuPortal,
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  Logo,
4
4
  uiRelease
5
- } from "./chunk-7JBLPGZ4.js";
5
+ } from "./chunk-JDHGKM25.js";
6
6
  import {
7
7
  Badge,
8
8
  Button,
@@ -13,12 +13,13 @@ import {
13
13
  DropdownMenuSeparator,
14
14
  DropdownMenuTrigger,
15
15
  Navigation,
16
+ NavigationSheetAction,
16
17
  Select,
17
18
  SelectContent,
18
19
  SelectItem,
19
20
  SelectTrigger,
20
21
  SelectValue
21
- } from "./chunk-QQQQZEPV.js";
22
+ } from "./chunk-KPE4KKYM.js";
22
23
  import {
23
24
  __objRest,
24
25
  __spreadProps,
@@ -177,6 +178,7 @@ var headerContent = {
177
178
 
178
179
  // src/components/naeil-header-account.tsx
179
180
  import { UserRound } from "lucide-react";
181
+ import { useContext, useRef } from "react";
180
182
  import { jsx, jsxs } from "react/jsx-runtime";
181
183
  var labels = {
182
184
  en: { menu: "Account menu", account: "Account", login: "Log in", logout: "Log out", busy: "Logging out\u2026" },
@@ -186,10 +188,18 @@ var labels = {
186
188
  var NativeLink = (props) => /* @__PURE__ */ jsx("a", __spreadValues({}, props));
187
189
  function NaeilHeaderAccountControl({ locale, account, Link = NativeLink }) {
188
190
  const text = labels[locale];
191
+ const handoff = useContext(NavigationSheetAction);
192
+ const pendingAction = useRef(void 0);
189
193
  if (account.status === "signed-out") {
190
194
  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 });
191
195
  }
192
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
196
+ return /* @__PURE__ */ jsxs(DropdownMenu, { onOpenChange: (open) => {
197
+ var _a;
198
+ if (open) {
199
+ (_a = pendingAction.current) == null ? void 0 : _a.cancel();
200
+ pendingAction.current = void 0;
201
+ }
202
+ }, children: [
193
203
  /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
194
204
  "button",
195
205
  {
@@ -202,24 +212,46 @@ function NaeilHeaderAccountControl({ locale, account, Link = NativeLink }) {
202
212
  children: /* @__PURE__ */ jsx("span", { className: "naeil-header-account-avatar", "aria-hidden": "true", children: /* @__PURE__ */ jsx(UserRound, {}) })
203
213
  }
204
214
  ) }),
205
- /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "w-56 max-w-[calc(100vw-2rem)]", "data-naeil-account-menu": true, children: [
206
- /* @__PURE__ */ jsxs(DropdownMenuLabel, { className: "flex min-w-0 flex-col font-normal", children: [
207
- /* @__PURE__ */ jsx("span", { className: "truncate text-base font-medium", children: account.displayName || text.account }),
208
- account.email && /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: account.email })
209
- ] }),
210
- /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
211
- /* @__PURE__ */ jsx(
212
- DropdownMenuItem,
213
- {
214
- className: "min-h-11",
215
- disabled: account.busy,
216
- onSelect: () => {
217
- if (!account.busy) account.onLogout();
218
- },
219
- children: account.busy ? text.busy : text.logout
220
- }
221
- )
222
- ] })
215
+ /* @__PURE__ */ jsxs(
216
+ DropdownMenuContent,
217
+ {
218
+ align: "end",
219
+ className: "w-56 max-w-[calc(100vw-2rem)]",
220
+ "data-naeil-account-menu": true,
221
+ "data-naeil-chrome": "",
222
+ lang: locale,
223
+ onCloseAutoFocus: (event) => {
224
+ const flow = pendingAction.current;
225
+ if (!flow || event.target !== flow.menu) return;
226
+ event.preventDefault();
227
+ flow.menu = null;
228
+ flow.menuClosed();
229
+ },
230
+ children: [
231
+ /* @__PURE__ */ jsxs(DropdownMenuLabel, { className: "flex min-w-0 flex-col font-normal", children: [
232
+ /* @__PURE__ */ jsx("span", { className: "truncate text-base font-medium", children: account.displayName || text.account }),
233
+ account.email && /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-muted-foreground", children: account.email })
234
+ ] }),
235
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
236
+ /* @__PURE__ */ jsx(
237
+ DropdownMenuItem,
238
+ {
239
+ className: "min-h-11",
240
+ disabled: account.busy,
241
+ onSelect: (event) => {
242
+ if (account.busy) return;
243
+ if (handoff) {
244
+ const menu = event.target.closest("[data-naeil-account-menu]");
245
+ if (!(menu == null ? void 0 : menu.isConnected)) return;
246
+ pendingAction.current = __spreadProps(__spreadValues({}, handoff(account.onLogout)), { menu });
247
+ } else account.onLogout();
248
+ },
249
+ children: account.busy ? text.busy : text.logout
250
+ }
251
+ )
252
+ ]
253
+ }
254
+ )
223
255
  ] });
224
256
  }
225
257
 
@@ -254,7 +286,7 @@ function NaeilHeaderLocaleControl({ locale, onLocaleChange }) {
254
286
  children: /* @__PURE__ */ jsx2(SelectValue, { children: locale.toUpperCase() })
255
287
  }
256
288
  ),
257
- /* @__PURE__ */ jsx2(SelectContent, { align: "end", children: headerLocales.map((value) => /* @__PURE__ */ jsx2(SelectItem, { value, className: "min-h-11", lang: value, children: headerLocaleNames[value] }, value)) })
289
+ /* @__PURE__ */ jsx2(SelectContent, { align: "end", "data-naeil-chrome": "", "data-naeil-locale-menu": "", lang: locale, children: headerLocales.map((value) => /* @__PURE__ */ jsx2(SelectItem, { value, className: "min-h-11", lang: value, children: headerLocaleNames[value] }, value)) })
258
290
  ] });
259
291
  }
260
292
  var NativeLink2 = (props) => /* @__PURE__ */ jsx2("a", __spreadValues({}, props));
@@ -264,6 +296,9 @@ function NaeilHeader(_a) {
264
296
  resolveHref = (path) => `https://naeil.dev/${locale}${path === "/" ? "" : path}`,
265
297
  brandHref,
266
298
  brandSlot,
299
+ brandSuffix,
300
+ statusSlot,
301
+ appNavigation,
267
302
  brandLinkProps,
268
303
  brandLinkComponent,
269
304
  onLocaleChange,
@@ -288,6 +323,9 @@ function NaeilHeader(_a) {
288
323
  "resolveHref",
289
324
  "brandHref",
290
325
  "brandSlot",
326
+ "brandSuffix",
327
+ "statusSlot",
328
+ "appNavigation",
291
329
  "brandLinkProps",
292
330
  "brandLinkComponent",
293
331
  "onLocaleChange",
@@ -330,41 +368,61 @@ function NaeilHeader(_a) {
330
368
  const brandDestination = brandHref != null ? brandHref : resolveHref("/");
331
369
  const BrandLink = brandLinkComponent != null ? brandLinkComponent : Link;
332
370
  const _a2 = brandLinkProps != null ? brandLinkProps : {}, { onClick: onBrandClick } = _a2, brandProps = __objRest(_a2, ["onClick"]);
333
- const toolbar = /* @__PURE__ */ jsx2("div", { "data-app-toolbar": true, className: "naeil-header-controls", children: toolbarSlot !== void 0 ? toolbarSlot : /* @__PURE__ */ jsxs2(Fragment, { children: [
334
- /* @__PURE__ */ jsx2(NaeilHeaderThemeControl, { locale }),
335
- onLocaleChange && /* @__PURE__ */ jsx2(NaeilHeaderLocaleControl, { locale, onLocaleChange }),
336
- authSlot && /* @__PURE__ */ jsxs2(Fragment, { children: [
337
- /* @__PURE__ */ jsx2("div", { "aria-hidden": "true", className: "naeil-header-separator" }),
338
- authSlot
339
- ] })
340
- ] }) });
371
+ const toolbar = /* @__PURE__ */ jsx2(
372
+ "div",
373
+ {
374
+ "data-app-toolbar": true,
375
+ className: "naeil-header-controls",
376
+ "data-naeil-chrome": toolbarSlot === void 0 ? "" : void 0,
377
+ lang: toolbarSlot === void 0 ? locale : void 0,
378
+ children: toolbarSlot !== void 0 ? toolbarSlot : /* @__PURE__ */ jsxs2(Fragment, { children: [
379
+ /* @__PURE__ */ jsx2(NaeilHeaderThemeControl, { locale }),
380
+ onLocaleChange && /* @__PURE__ */ jsx2(NaeilHeaderLocaleControl, { locale, onLocaleChange }),
381
+ authSlot && /* @__PURE__ */ jsxs2(Fragment, { children: [
382
+ /* @__PURE__ */ jsx2("div", { "aria-hidden": "true", className: "naeil-header-separator" }),
383
+ authSlot
384
+ ] })
385
+ ] })
386
+ }
387
+ );
388
+ const logo = /* @__PURE__ */ jsx2(Logo, { variant: "horizontal", size: 28, className: "block", label: null });
389
+ const brand = /* @__PURE__ */ jsx2(
390
+ BrandLink,
391
+ __spreadProps(__spreadValues({
392
+ href: brandDestination,
393
+ "data-app-brand": true,
394
+ "aria-label": brandSlot == null ? ".naeil" : void 0
395
+ }, brandProps), {
396
+ onClick: (event) => {
397
+ onBrandClick == null ? void 0 : onBrandClick(event);
398
+ onNavigate == null ? void 0 : onNavigate(event, { id: "brand", href: brandDestination, label: ".naeil" });
399
+ },
400
+ children: brandSlot != null ? brandSlot : brandSuffix ? /* @__PURE__ */ jsxs2(Fragment, { children: [
401
+ logo,
402
+ /* @__PURE__ */ jsx2("span", { className: "naeil-header-brand-suffix", children: brandSuffix })
403
+ ] }) : logo
404
+ })
405
+ );
341
406
  return /* @__PURE__ */ jsx2(
342
407
  Navigation,
343
408
  __spreadProps(__spreadValues({}, props), {
344
409
  "data-global-header": true,
345
410
  "data-naeil-header": true,
411
+ "data-naeil-chrome": "",
412
+ lang: locale,
346
413
  "data-naeil-ui-version": uiRelease.version,
347
414
  "data-naeil-ui-contract": uiRelease.contractVersion,
348
415
  Link,
349
416
  currentHref,
350
417
  onNavigate,
351
- brand: /* @__PURE__ */ jsx2(
352
- BrandLink,
353
- __spreadProps(__spreadValues({
354
- href: brandDestination,
355
- "data-app-brand": true,
356
- "aria-label": brandSlot == null ? ".naeil" : void 0
357
- }, brandProps), {
358
- onClick: (event) => {
359
- onBrandClick == null ? void 0 : onBrandClick(event);
360
- onNavigate == null ? void 0 : onNavigate(event, { id: "brand", href: brandDestination, label: ".naeil" });
361
- },
362
- children: brandSlot != null ? brandSlot : /* @__PURE__ */ jsx2(Logo, { variant: "horizontal", size: 28, className: "block", label: null })
363
- })
364
- ),
418
+ brand: statusSlot != null ? /* @__PURE__ */ jsxs2("div", { className: "naeil-header-brand-row", children: [
419
+ brand,
420
+ statusSlot
421
+ ] }) : brand,
365
422
  items: [
366
423
  ...showProjects ? [{ id: "projects", label: (_b2 = translations == null ? void 0 : translations.projects) != null ? _b2 : content.nav.projects, items: projectLinks, columns: projectLinks.length > 1 ? 2 : 1 }] : [],
367
424
  ...showBlog ? [{ id: "blog", label: (_c = translations == null ? void 0 : translations.blog) != null ? _c : content.nav.blog, href: resolveHref("/blog"), external: projectsExternal }] : [],
425
+ ...appNavigation != null ? appNavigation : [],
368
426
  ...(subNav != null ? subNav : []).map((item) => __spreadProps(__spreadValues({}, item), { id: item.href }))
369
427
  ],
370
428
  labels: labels2 != null ? labels2 : { navigation: content.nav.navigation, menu: content.nav.menu, close: content.nav.closeMenu },
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { N as NavigationLink, a as NavigationProps } from '../navigation-CgPeL9tF.js';
3
- import { S as SiteFooterProps } from '../site-footer-vXDa_6yr.js';
2
+ import { N as NavigationLink, a as NavigationProps } from '../navigation-D8N1tMTu.js';
3
+ import { S as SiteFooterProps } from '../site-footer-s113FZgC.js';
4
4
  import { ThemeProvider as ThemeProvider$1 } from 'next-themes';
5
5
  import * as React$1 from 'react';
6
6
  import { SVGProps } from 'react';
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  NaeilHeader,
4
4
  headerLocales
5
- } from "../chunk-MGCCKOGU.js";
5
+ } from "../chunk-XRO3NJAB.js";
6
6
  import {
7
7
  Logo
8
- } from "../chunk-7JBLPGZ4.js";
8
+ } from "../chunk-JDHGKM25.js";
9
9
  import {
10
10
  SiteFooter
11
11
  } from "../chunk-HSTDPY56.js";
@@ -15,7 +15,7 @@ import {
15
15
  DropdownMenuContent,
16
16
  DropdownMenuItem,
17
17
  DropdownMenuTrigger
18
- } from "../chunk-QQQQZEPV.js";
18
+ } from "../chunk-KPE4KKYM.js";
19
19
  import {
20
20
  __objRest,
21
21
  __spreadProps,
@@ -1,8 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { S as SiteFooterProps } from '../site-footer-vXDa_6yr.js';
2
+ import { S as SiteFooterProps } from '../site-footer-s113FZgC.js';
3
3
  import { N as NaeilHeaderLocale } from '../naeil-header-content-BvISJ4RV.js';
4
4
  import 'react';
5
- import '../navigation-CgPeL9tF.js';
5
+ import '../navigation-D8N1tMTu.js';
6
6
 
7
7
  type NaeilFooterLocale = NaeilHeaderLocale;
8
8
  interface NaeilFooterProps extends Omit<SiteFooterProps, "brand" | "description" | "groups"> {
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  Logo,
5
5
  uiRelease
6
- } from "../chunk-7JBLPGZ4.js";
6
+ } from "../chunk-JDHGKM25.js";
7
7
  import {
8
8
  SiteFooter
9
9
  } from "../chunk-HSTDPY56.js";
@@ -44,6 +44,8 @@ function NaeilFooter(_a) {
44
44
  Link,
45
45
  onNavigate,
46
46
  "data-naeil-footer": true,
47
+ "data-naeil-chrome": "",
48
+ lang: locale,
47
49
  "data-naeil-ui-version": uiRelease.version,
48
50
  "data-naeil-ui-contract": uiRelease.contractVersion,
49
51
  brand: /* @__PURE__ */ jsx(