@ubx/docs-ui 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Header.d.ts +8 -2
- package/dist/Header.js +3 -3
- package/dist/MobileNav.d.ts +5 -1
- package/dist/MobileNav.js +46 -14
- package/dist/PageShell.d.ts +12 -5
- package/dist/PageShell.js +19 -10
- package/dist/ThemeToggle.js +13 -7
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/package.json +1 -1
- package/dist/MobileSidebarToggle.d.ts +0 -8
- package/dist/MobileSidebarToggle.js +0 -90
package/dist/Header.d.ts
CHANGED
|
@@ -8,13 +8,19 @@ export type SectionTab = {
|
|
|
8
8
|
label: string;
|
|
9
9
|
href: string;
|
|
10
10
|
};
|
|
11
|
-
export declare function Header({ nav, tabs, activeTab,
|
|
11
|
+
export declare function Header({ nav, tabs, activeTab, sidebar, sidebarLabel, search, showThemeToggle, githubUrl, }: {
|
|
12
12
|
nav: NavLink[];
|
|
13
13
|
/** Omit entirely for a single-tier header, which is the provider site. */
|
|
14
14
|
tabs?: SectionTab[];
|
|
15
15
|
/** href of the active tab, matched by prefix so nested pages stay lit. */
|
|
16
16
|
activeTab?: string;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The section tree, when the page has one. Goes into the mobile
|
|
19
|
+
* drawer; the desktop rail is PageShell's, not the header's.
|
|
20
|
+
*/
|
|
21
|
+
sidebar?: React.ReactNode;
|
|
22
|
+
/** Heading for the tree inside the drawer, in the site's own words. */
|
|
23
|
+
sidebarLabel?: string;
|
|
18
24
|
/**
|
|
19
25
|
* Search control, rendered to the left of the theme toggle.
|
|
20
26
|
*
|
package/dist/Header.js
CHANGED
|
@@ -2,13 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { ThemeToggle } from "./ThemeToggle";
|
|
4
4
|
import { MobileNav } from "./MobileNav";
|
|
5
|
-
export function Header({ nav, tabs, activeTab,
|
|
6
|
-
return (_jsxs("header", { className: "border-b border-border bg-background", children: [_jsxs("div", { className: "relative mx-auto flex max-w-7xl items-center gap-3 px-6 py-4", children: [
|
|
5
|
+
export function Header({ nav, tabs, activeTab, sidebar, sidebarLabel, search, showThemeToggle = true, githubUrl, }) {
|
|
6
|
+
return (_jsxs("header", { className: "border-b border-border bg-background", children: [_jsxs("div", { className: "relative mx-auto flex max-w-7xl items-center gap-3 px-6 py-4", children: [_jsx(Link, { href: "/", className: "flex shrink-0 items-center", children: _jsx("img", { src: "/logo/ubiquex.png", alt: "Ubiquex", className: "h-6 w-auto" }) }), _jsx("div", { className: "flex-1" }), _jsx("nav", { className: "absolute left-1/2 hidden -translate-x-1/2 items-center gap-5 md:flex", children: nav.map((item) => {
|
|
7
7
|
const className = item.current
|
|
8
8
|
? "text-sm text-primary"
|
|
9
9
|
: "text-sm text-foreground-muted hover:text-primary";
|
|
10
10
|
return item.href.startsWith("/") ? (_jsx(Link, { href: item.href, className: className, children: item.label }, item.label)) : (_jsx("a", { href: item.href, className: className, children: item.label }, item.label));
|
|
11
|
-
}) }), search ? _jsx("div", { className: "w-40 shrink-0 sm:w-56 lg:w-64", children: search }) : null, githubUrl ? (_jsxs("a", { href: githubUrl, className: "hidden shrink-0 items-center gap-2 rounded-full bg-primary px-4 py-[7px] text-sm text-primary-foreground transition-opacity hover:opacity-90 sm:flex", children: [_jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" }) }), "Star us on GitHub"] })) : null, showThemeToggle ? _jsx(ThemeToggle, {}) : null, _jsx(MobileNav, { nav: nav, githubUrl: githubUrl })] }), tabs && tabs.length > 0 ? (_jsx("div", { className: "mx-auto max-w-7xl px-6", children: _jsx("nav", { className: "flex gap-6 overflow-x-auto", children: tabs.map((tab) => {
|
|
11
|
+
}) }), search ? _jsx("div", { className: "w-40 shrink-0 sm:w-56 lg:w-64", children: search }) : null, githubUrl ? (_jsxs("a", { href: githubUrl, className: "hidden shrink-0 items-center gap-2 rounded-full bg-primary px-4 py-[7px] text-sm text-primary-foreground transition-opacity hover:opacity-90 sm:flex", children: [_jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" }) }), "Star us on GitHub"] })) : null, showThemeToggle ? (_jsx("div", { className: "hidden md:block", children: _jsx(ThemeToggle, {}) })) : null, _jsx(MobileNav, { nav: nav, githubUrl: githubUrl, sidebar: sidebar, sidebarLabel: sidebarLabel, showThemeToggle: showThemeToggle })] }), tabs && tabs.length > 0 ? (_jsx("div", { className: "mx-auto max-w-7xl px-6", children: _jsx("nav", { className: "flex gap-6 overflow-x-auto", children: tabs.map((tab) => {
|
|
12
12
|
// Prefix match, so /concepts/ledger keeps the Concepts tab
|
|
13
13
|
// lit rather than only the exact section index.
|
|
14
14
|
const active = activeTab === tab.href || activeTab?.startsWith(tab.href + "/");
|
package/dist/MobileNav.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { NavLink } from "./Header";
|
|
2
|
-
export declare function MobileNav({ nav, githubUrl }: {
|
|
2
|
+
export declare function MobileNav({ nav, githubUrl, sidebar, sidebarLabel, showThemeToggle, }: {
|
|
3
3
|
nav: NavLink[];
|
|
4
4
|
githubUrl?: string;
|
|
5
|
+
/** The section tree, when the page has one. Rendered below the destinations. */
|
|
6
|
+
sidebar?: React.ReactNode;
|
|
7
|
+
sidebarLabel?: string;
|
|
8
|
+
showThemeToggle?: boolean;
|
|
5
9
|
}): import("react").JSX.Element;
|
package/dist/MobileNav.js
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
// only visible header link was the logo. There was no route to Install,
|
|
8
|
-
// Documentation, Tutorials, Providers or Blog on a phone, anywhere.
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { ThemeToggle } from "./ThemeToggle";
|
|
6
|
+
// The one mobile navigation control on all three sites.
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
8
|
+
// It used to be two. The header's destination nav is `hidden md:flex`,
|
|
9
|
+
// so below 768px it was gone, and this drawer was added to reach it.
|
|
10
|
+
// But the docs sites ALSO rendered MobileSidebarToggle, a second burger
|
|
11
|
+
// on the left opening a second drawer, for the section tree. A reader on
|
|
12
|
+
// a phone met two hamburger buttons that looked identical, opened from
|
|
13
|
+
// opposite edges, and each held half the navigation.
|
|
14
|
+
//
|
|
15
|
+
// So this component now holds both: destinations first, then the section
|
|
16
|
+
// tree when the page has one. MobileSidebarToggle is gone rather than
|
|
17
|
+
// kept alongside, because two implementations of one drawer is how the
|
|
18
|
+
// two drifted in the first place.
|
|
19
|
+
//
|
|
20
|
+
// Opens DOWNWARD from below the header rather than in from an edge. A
|
|
21
|
+
// side sheet reads as a separate surface that arrived over the page; a
|
|
22
|
+
// panel dropping from the bar reads as the bar itself expanding, which
|
|
23
|
+
// is what it is.
|
|
13
24
|
const TRANSITION_MS = 200;
|
|
14
|
-
export function MobileNav({ nav, githubUrl }) {
|
|
25
|
+
export function MobileNav({ nav, githubUrl, sidebar, sidebarLabel, showThemeToggle = true, }) {
|
|
15
26
|
const [mounted, setMounted] = useState(false);
|
|
16
27
|
const [visible, setVisible] = useState(false);
|
|
28
|
+
// Where the header actually ends, measured rather than assumed. The
|
|
29
|
+
// header is one tier on the marketing site and two on the docs sites
|
|
30
|
+
// (destinations plus a section tab strip), and a hardcoded offset
|
|
31
|
+
// would drop the panel over the tab strip on exactly the sites that
|
|
32
|
+
// have one.
|
|
33
|
+
const [top, setTop] = useState(0);
|
|
34
|
+
const buttonRef = useRef(null);
|
|
17
35
|
useEffect(() => {
|
|
18
36
|
if (!mounted)
|
|
19
37
|
return;
|
|
@@ -23,10 +41,23 @@ export function MobileNav({ nav, githubUrl }) {
|
|
|
23
41
|
if (e.key === "Escape")
|
|
24
42
|
close();
|
|
25
43
|
};
|
|
44
|
+
// Re-measure on resize: rotating a phone changes the header's
|
|
45
|
+
// height whenever anything in it wraps.
|
|
46
|
+
const onResize = () => {
|
|
47
|
+
const header = buttonRef.current?.closest("header");
|
|
48
|
+
if (header)
|
|
49
|
+
setTop(header.getBoundingClientRect().bottom);
|
|
50
|
+
};
|
|
26
51
|
document.addEventListener("keydown", onKey);
|
|
27
|
-
|
|
52
|
+
window.addEventListener("resize", onResize);
|
|
53
|
+
return () => {
|
|
54
|
+
document.removeEventListener("keydown", onKey);
|
|
55
|
+
window.removeEventListener("resize", onResize);
|
|
56
|
+
};
|
|
28
57
|
}, [mounted]);
|
|
29
58
|
function open() {
|
|
59
|
+
const header = buttonRef.current?.closest("header");
|
|
60
|
+
setTop(header ? header.getBoundingClientRect().bottom : 0);
|
|
30
61
|
setMounted(true);
|
|
31
62
|
requestAnimationFrame(() => requestAnimationFrame(() => setVisible(true)));
|
|
32
63
|
}
|
|
@@ -34,8 +65,9 @@ export function MobileNav({ nav, githubUrl }) {
|
|
|
34
65
|
setVisible(false);
|
|
35
66
|
setTimeout(() => setMounted(false), TRANSITION_MS);
|
|
36
67
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
68
|
+
const linkClass = (current) => current ? "py-2 text-primary" : "py-2 text-foreground-muted hover:text-primary";
|
|
69
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: buttonRef, type: "button", onClick: mounted ? close : open, "aria-label": mounted ? "Close navigation" : "Open navigation", "aria-expanded": mounted, className: "flex h-9 w-9 shrink-0 items-center justify-center rounded text-foreground-muted hover:bg-surface hover:text-primary md:hidden", children: mounted ? (_jsx("svg", { viewBox: "0 0 20 20", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) })) : (_jsx("svg", { viewBox: "0 0 20 20", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M3 5.5h14M3 10h14M3 14.5h14", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) })) }), mounted && (_jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": "Navigation", className: "fixed inset-x-0 bottom-0 z-50 md:hidden", style: { top }, children: [_jsx("button", { type: "button", "aria-label": "Close navigation", onClick: close, className: "absolute inset-0 bg-foreground/40 transition-opacity duration-200 motion-reduce:transition-none " +
|
|
70
|
+
(visible ? "opacity-100" : "opacity-0") }), _jsx("div", { className: "absolute inset-x-0 top-0 max-h-full overflow-hidden", children: _jsxs("div", { className: "flex max-h-[calc(100vh-8rem)] flex-col gap-1 overflow-y-auto border-b border-border bg-background px-6 pt-4 pb-6 shadow-lg " +
|
|
71
|
+
"transition-transform duration-200 ease-out motion-reduce:transition-none " +
|
|
72
|
+
(visible ? "translate-y-0" : "-translate-y-full"), children: [nav.map((item) => item.href.startsWith("/") ? (_jsx(Link, { href: item.href, onClick: close, className: linkClass(item.current), children: item.label }, item.label)) : (_jsx("a", { href: item.href, onClick: close, className: linkClass(item.current), children: item.label }, item.label))), sidebar ? (_jsxs("div", { className: "mt-4 border-t border-border pt-4", children: [sidebarLabel ? (_jsx("div", { className: "mb-2 text-xs tracking-wide text-foreground-muted uppercase", children: sidebarLabel })) : null, _jsx("div", { onClick: close, children: sidebar })] })) : null, githubUrl ? (_jsx("a", { href: githubUrl, className: "mt-4 rounded-full bg-primary px-4 py-2 text-center text-sm text-primary-foreground", children: "Star us on GitHub" })) : null, showThemeToggle ? (_jsxs("div", { className: "mt-4 flex items-center justify-between border-t border-border pt-4", children: [_jsx("span", { className: "text-sm text-foreground-muted", children: "Theme" }), _jsx(ThemeToggle, {})] })) : null] }) })] }))] }));
|
|
41
73
|
}
|
package/dist/PageShell.d.ts
CHANGED
|
@@ -78,16 +78,23 @@ export type PageShellProps = {
|
|
|
78
78
|
};
|
|
79
79
|
export declare function PageShell({ nav, tabs, activeTab, sidebar, sidebarLabel, searchPlaceholder, searchPlacement, intro, showThemeToggle, githubUrl, fullBleed, footer, children, }: PageShellProps): React.JSX.Element;
|
|
80
80
|
/**
|
|
81
|
-
* Sets data-theme
|
|
81
|
+
* Sets data-theme before paint.
|
|
82
82
|
*
|
|
83
83
|
* Both sites carried this as a verbatim string constant in their own root
|
|
84
84
|
* layout, which is duplication the package could not see. It has to be an
|
|
85
85
|
* exported string rather than a component because it goes in <head> via
|
|
86
86
|
* dangerouslySetInnerHTML and must run ahead of hydration: a React
|
|
87
87
|
* component would run too late and the reader would see a flash of the
|
|
88
|
-
*
|
|
88
|
+
* default before their stored choice applied.
|
|
89
89
|
*
|
|
90
|
-
*
|
|
91
|
-
* "follow the OS".
|
|
90
|
+
* DARK IS THE DEFAULT, and absent storage now means dark rather than
|
|
91
|
+
* "follow the OS". The three sites are one product, and which one a
|
|
92
|
+
* reader saw first decided whether it was a dark product or a light one.
|
|
93
|
+
*
|
|
94
|
+
* "system" is still a real, selectable state; it is now an explicit
|
|
95
|
+
* stored choice rather than the absence of one. That distinction is why
|
|
96
|
+
* the key is written on every choice below rather than removed for
|
|
97
|
+
* system: with dark as the default, removing the key would mean dark,
|
|
98
|
+
* not system, and the option would silently do nothing.
|
|
92
99
|
*/
|
|
93
|
-
export declare const THEME_INIT_SCRIPT = "(function () {\n try {\n
|
|
100
|
+
export declare const THEME_INIT_SCRIPT = "(function () {\n var stored = null;\n try {\n stored = window.localStorage.getItem(\"ubx-docs-theme\");\n } catch (e) {}\n if (stored === \"light\" || stored === \"dark\") {\n document.documentElement.setAttribute(\"data-theme\", stored);\n } else if (stored !== \"system\") {\n document.documentElement.setAttribute(\"data-theme\", \"dark\");\n }\n})();";
|
package/dist/PageShell.js
CHANGED
|
@@ -2,7 +2,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Header } from "./Header";
|
|
3
3
|
import { Footer } from "./Footer";
|
|
4
4
|
import { GlobalSearch } from "./GlobalSearch";
|
|
5
|
-
import { MobileSidebarToggle } from "./MobileSidebarToggle";
|
|
6
5
|
/** Compact, for sitting beside the theme toggle. */
|
|
7
6
|
const HEADER_SEARCH_CLASS = "w-full rounded-full bg-field px-4 py-1.5 text-sm text-foreground outline-none " +
|
|
8
7
|
"placeholder:text-foreground-muted focus:ring-2 focus:ring-primary/30";
|
|
@@ -12,29 +11,39 @@ const HERO_SEARCH_CLASS = "w-full rounded-full bg-field px-5 py-3 text-base text
|
|
|
12
11
|
export function PageShell({ nav, tabs, activeTab, sidebar, sidebarLabel = "Navigation", searchPlaceholder, searchPlacement = "header", intro, showThemeToggle = true, githubUrl, fullBleed = false, footer, children, }) {
|
|
13
12
|
const headerSearch = searchPlaceholder && searchPlacement === "header" ? (_jsx(GlobalSearch, { placeholder: searchPlaceholder, inputClassName: HEADER_SEARCH_CLASS })) : undefined;
|
|
14
13
|
const heroSearch = searchPlaceholder && searchPlacement === "hero" ? (_jsx("div", { className: "mx-auto mt-8 max-w-xl", children: _jsx(GlobalSearch, { placeholder: searchPlaceholder, inputClassName: HERO_SEARCH_CLASS }) })) : null;
|
|
15
|
-
return (_jsxs(_Fragment, { children: [_jsx(Header, { nav: nav, tabs: tabs, activeTab: activeTab, search: headerSearch, showThemeToggle: showThemeToggle, githubUrl: githubUrl,
|
|
14
|
+
return (_jsxs(_Fragment, { children: [_jsx(Header, { nav: nav, tabs: tabs, activeTab: activeTab, search: headerSearch, showThemeToggle: showThemeToggle, githubUrl: githubUrl, sidebar: sidebar, sidebarLabel: sidebarLabel }), sidebar ? (
|
|
16
15
|
// No intro or hero search here on purpose: a page with a sidebar
|
|
17
16
|
// is a content page, and a hero belongs to a landing page.
|
|
18
17
|
_jsxs("div", { className: "mx-auto flex w-full max-w-7xl flex-1 gap-10 px-6 py-10", children: [_jsx("aside", { className: "hidden w-64 shrink-0 lg:block", children: sidebar }), _jsx("main", { className: "min-w-0 flex-1", children: children })] })) : fullBleed ? (_jsxs("main", { className: "flex-1", children: [intro, heroSearch, children] })) : (_jsxs("main", { className: "mx-auto w-full max-w-7xl flex-1 px-6 py-12", children: [intro, heroSearch, children] })), _jsx(Footer, { tagline: footer.tagline, links: footer.links })] }));
|
|
19
18
|
}
|
|
20
19
|
/**
|
|
21
|
-
* Sets data-theme
|
|
20
|
+
* Sets data-theme before paint.
|
|
22
21
|
*
|
|
23
22
|
* Both sites carried this as a verbatim string constant in their own root
|
|
24
23
|
* layout, which is duplication the package could not see. It has to be an
|
|
25
24
|
* exported string rather than a component because it goes in <head> via
|
|
26
25
|
* dangerouslySetInnerHTML and must run ahead of hydration: a React
|
|
27
26
|
* component would run too late and the reader would see a flash of the
|
|
28
|
-
*
|
|
27
|
+
* default before their stored choice applied.
|
|
29
28
|
*
|
|
30
|
-
*
|
|
31
|
-
* "follow the OS".
|
|
29
|
+
* DARK IS THE DEFAULT, and absent storage now means dark rather than
|
|
30
|
+
* "follow the OS". The three sites are one product, and which one a
|
|
31
|
+
* reader saw first decided whether it was a dark product or a light one.
|
|
32
|
+
*
|
|
33
|
+
* "system" is still a real, selectable state; it is now an explicit
|
|
34
|
+
* stored choice rather than the absence of one. That distinction is why
|
|
35
|
+
* the key is written on every choice below rather than removed for
|
|
36
|
+
* system: with dark as the default, removing the key would mean dark,
|
|
37
|
+
* not system, and the option would silently do nothing.
|
|
32
38
|
*/
|
|
33
39
|
export const THEME_INIT_SCRIPT = `(function () {
|
|
40
|
+
var stored = null;
|
|
34
41
|
try {
|
|
35
|
-
|
|
36
|
-
if (stored === "light" || stored === "dark") {
|
|
37
|
-
document.documentElement.setAttribute("data-theme", stored);
|
|
38
|
-
}
|
|
42
|
+
stored = window.localStorage.getItem("ubx-docs-theme");
|
|
39
43
|
} catch (e) {}
|
|
44
|
+
if (stored === "light" || stored === "dark") {
|
|
45
|
+
document.documentElement.setAttribute("data-theme", stored);
|
|
46
|
+
} else if (stored !== "system") {
|
|
47
|
+
document.documentElement.setAttribute("data-theme", "dark");
|
|
48
|
+
}
|
|
40
49
|
})();`;
|
package/dist/ThemeToggle.js
CHANGED
|
@@ -29,6 +29,7 @@ function subscribe(callback) {
|
|
|
29
29
|
window.removeEventListener("ubx-theme-change", callback);
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
|
+
const DEFAULT = "dark";
|
|
32
33
|
function getSnapshot() {
|
|
33
34
|
// Same real risk app/layout.tsx's own inline THEME_INIT_SCRIPT
|
|
34
35
|
// already guards against for the identical read -- private
|
|
@@ -41,14 +42,18 @@ function getSnapshot() {
|
|
|
41
42
|
// control.
|
|
42
43
|
try {
|
|
43
44
|
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
44
|
-
return stored === "light" || stored === "dark"
|
|
45
|
+
return stored === "light" || stored === "dark" || stored === "system" ? stored : DEFAULT;
|
|
45
46
|
}
|
|
46
47
|
catch {
|
|
47
|
-
return
|
|
48
|
+
return DEFAULT;
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
function getServerSnapshot() {
|
|
51
|
-
|
|
52
|
+
// The server has no localStorage, and this is also the correct first
|
|
53
|
+
// client answer: THEME_INIT_SCRIPT has already put the real
|
|
54
|
+
// data-theme on the document by the time this renders, so only this
|
|
55
|
+
// control's own highlight can lag, never the page.
|
|
56
|
+
return DEFAULT;
|
|
52
57
|
}
|
|
53
58
|
function SystemIcon() {
|
|
54
59
|
return (_jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: [_jsx("rect", { x: "1.5", y: "2.5", width: "13", height: "8.5", rx: "1", stroke: "currentColor", strokeWidth: "1.3" }), _jsx("path", { d: "M5.5 14h5M8 11v3", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" })] }));
|
|
@@ -68,15 +73,16 @@ const OPTIONS = [
|
|
|
68
73
|
// always visible -- replaces the earlier single cycling button (whose
|
|
69
74
|
// current state was legible only from its text label, distinct icons
|
|
70
75
|
// were the whole point of this pass).
|
|
76
|
+
//
|
|
77
|
+
// Rendered in the desktop header and, below md, inside the mobile
|
|
78
|
+
// drawer instead. Three icons plus a logo, a burger and a call to
|
|
79
|
+
// action did not fit a phone-width bar.
|
|
71
80
|
export function ThemeToggle() {
|
|
72
81
|
const choice = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
73
82
|
function choose(next) {
|
|
74
83
|
applyTheme(next);
|
|
75
84
|
try {
|
|
76
|
-
|
|
77
|
-
window.localStorage.removeItem(STORAGE_KEY);
|
|
78
|
-
else
|
|
79
|
-
window.localStorage.setItem(STORAGE_KEY, next);
|
|
85
|
+
window.localStorage.setItem(STORAGE_KEY, next);
|
|
80
86
|
}
|
|
81
87
|
catch {
|
|
82
88
|
// Storage blocked -- the choice still applies to this page via
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export { GlobalSearch } from "./GlobalSearch";
|
|
|
7
7
|
export type { SearchEntry } from "./GlobalSearch";
|
|
8
8
|
export { ThemeToggle } from "./ThemeToggle";
|
|
9
9
|
export { Footer } from "./Footer";
|
|
10
|
-
export { MobileSidebarToggle } from "./MobileSidebarToggle";
|
|
11
10
|
export { PageShell, THEME_INIT_SCRIPT } from "./PageShell";
|
|
12
11
|
export type { PageShellProps } from "./PageShell";
|
|
13
12
|
export { MobileNav } from "./MobileNav";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Extracted only after both consumers existed, deliberately. Three of
|
|
5
5
|
// these interfaces had to change to be shareable at all, and one of
|
|
6
|
-
// those was NOT predicted: MobileSidebarToggle imported ProviderSidebar
|
|
7
6
|
// directly, which was found when the second site failed to build.
|
|
8
7
|
// Extracting against a single call site would have carried that coupling
|
|
9
8
|
// into this package unnoticed.
|
|
@@ -13,6 +12,5 @@ export { Header } from "./Header";
|
|
|
13
12
|
export { GlobalSearch } from "./GlobalSearch";
|
|
14
13
|
export { ThemeToggle } from "./ThemeToggle";
|
|
15
14
|
export { Footer } from "./Footer";
|
|
16
|
-
export { MobileSidebarToggle } from "./MobileSidebarToggle";
|
|
17
15
|
export { PageShell, THEME_INIT_SCRIPT } from "./PageShell";
|
|
18
16
|
export { MobileNav } from "./MobileNav";
|
package/package.json
CHANGED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare function MobileSidebarToggle({ children, label, }: {
|
|
2
|
-
children: React.ReactNode;
|
|
3
|
-
/**
|
|
4
|
-
* What this drawer contains, in the consuming site's own vocabulary.
|
|
5
|
-
* Used as the drawer heading and as the basis for both aria-labels.
|
|
6
|
-
*/
|
|
7
|
-
label: string;
|
|
8
|
-
}): import("react").JSX.Element;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { usePathname } from "next/navigation";
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
5
|
-
// Opens from the header, since the desktop sidebar (ProviderSidebar's
|
|
6
|
-
// own "hidden lg:block" rail) contributes nothing below that
|
|
7
|
-
// breakpoint -- without this, a reader on mobile has no way to reach
|
|
8
|
-
// a provider's service groups or resources at all once past the
|
|
9
|
-
// provider home page. Renders the identical ProviderSidebar (same
|
|
10
|
-
// fetch, same filter, same tree) inside a drawer rather than a
|
|
11
|
-
// second, parallel mobile nav that could drift from the real one.
|
|
12
|
-
//
|
|
13
|
-
// Two-state open/close (mounted + visible, not a single boolean) so
|
|
14
|
-
// the close transition has time to actually play: closing flips
|
|
15
|
-
// `visible` off immediately (the CSS transition animates toward the
|
|
16
|
-
// closed position) and only removes the drawer from the DOM once that
|
|
17
|
-
// transition has had time to finish, rather than the element
|
|
18
|
-
// vanishing the instant the tap registers.
|
|
19
|
-
const TRANSITION_MS = 200;
|
|
20
|
-
// UBI-247: takes the drawer contents as `children` rather than
|
|
21
|
-
// constructing a ProviderSidebar itself. The provider site passes
|
|
22
|
-
// <ProviderSidebar .../>, this site passes <DocSidebar .../>, and the
|
|
23
|
-
// open/close/transition/route-change behaviour below is identical for
|
|
24
|
-
// both.
|
|
25
|
-
//
|
|
26
|
-
// This comment used to claim that the `children` coupling "was the one
|
|
27
|
-
// thing in this component that was not generic." That was wrong, and it
|
|
28
|
-
// is left recorded here rather than quietly deleted because the way it
|
|
29
|
-
// was wrong is the useful part. Three strings a few lines below still
|
|
30
|
-
// said "Services" and "service navigation", which is AWS provider
|
|
31
|
-
// vocabulary: the provider site's mobile drawer holds a provider's
|
|
32
|
-
// service groups. The user docs site's drawer holds documentation
|
|
33
|
-
// pages, so it announced the wrong thing to every mobile reader and to
|
|
34
|
-
// every screen reader.
|
|
35
|
-
//
|
|
36
|
-
// It survived because the extraction was audited per component by
|
|
37
|
-
// reading each one, and reading is exactly what does not catch a
|
|
38
|
-
// plausible-sounding noun. It was found later by mechanically
|
|
39
|
-
// extracting every user-visible string literal in the package and
|
|
40
|
-
// asking of each one whether it could be true on both sites. `label` is
|
|
41
|
-
// required for the same reason Footer's props are: a default here is a
|
|
42
|
-
// silent claim about which site you are.
|
|
43
|
-
export function MobileSidebarToggle({ children, label, }) {
|
|
44
|
-
const [mounted, setMounted] = useState(false);
|
|
45
|
-
const [visible, setVisible] = useState(false);
|
|
46
|
-
const closeTimer = useRef(null);
|
|
47
|
-
const pathname = usePathname();
|
|
48
|
-
const isFirstPathname = useRef(true);
|
|
49
|
-
function clearCloseTimer() {
|
|
50
|
-
if (closeTimer.current) {
|
|
51
|
-
clearTimeout(closeTimer.current);
|
|
52
|
-
closeTimer.current = null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function open() {
|
|
56
|
-
clearCloseTimer();
|
|
57
|
-
setMounted(true);
|
|
58
|
-
// Mounts in the closed visual position first -- flipping to
|
|
59
|
-
// `visible` one frame later gives the browser an actual "from"
|
|
60
|
-
// state to transition out of, rather than painting already open.
|
|
61
|
-
requestAnimationFrame(() => {
|
|
62
|
-
requestAnimationFrame(() => setVisible(true));
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
function close() {
|
|
66
|
-
setVisible(false);
|
|
67
|
-
clearCloseTimer();
|
|
68
|
-
closeTimer.current = setTimeout(() => setMounted(false), TRANSITION_MS);
|
|
69
|
-
}
|
|
70
|
-
useEffect(() => clearCloseTimer, []);
|
|
71
|
-
// Explicit close-on-navigation rather than relying on this
|
|
72
|
-
// component happening to remount between routes -- Header sits
|
|
73
|
-
// directly in each page.tsx, not behind a shared layout boundary
|
|
74
|
-
// between different resource pages, so whether React actually tears
|
|
75
|
-
// this instance down on every navigation isn't guaranteed. Skips
|
|
76
|
-
// the very first render (mount already starts closed) so this only
|
|
77
|
-
// ever fires on a real route change.
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (isFirstPathname.current) {
|
|
80
|
-
isFirstPathname.current = false;
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
close();
|
|
84
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
|
-
}, [pathname]);
|
|
86
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: open, "aria-label": `Open ${label} navigation`, className: "flex h-9 w-9 shrink-0 items-center justify-center rounded text-foreground-muted hover:bg-surface hover:text-primary lg:hidden", children: _jsx("svg", { viewBox: "0 0 20 20", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M3 5.5h14M3 10h14M3 14.5h14", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) }), mounted && (_jsxs("div", { role: "dialog", "aria-modal": "true", className: "fixed inset-0 z-50 lg:hidden", children: [_jsx("button", { type: "button", "aria-label": `Close ${label} navigation`, onClick: close, className: "absolute inset-0 bg-foreground/40 transition-opacity duration-200 motion-reduce:transition-none " +
|
|
87
|
-
(visible ? "opacity-100" : "opacity-0") }), _jsxs("div", { className: "absolute inset-y-0 left-0 flex w-80 max-w-[85vw] flex-col overflow-y-auto bg-background p-4 shadow-lg " +
|
|
88
|
-
"transition-transform duration-200 ease-out motion-reduce:transition-none " +
|
|
89
|
-
(visible ? "translate-x-0" : "-translate-x-full"), children: [_jsxs("div", { className: "mb-3 flex items-center justify-between", children: [_jsx("span", { className: "text-sm font-semibold text-foreground", children: label }), _jsx("button", { type: "button", onClick: close, "aria-label": `Close ${label} navigation`, className: "flex h-8 w-8 items-center justify-center rounded text-foreground-muted hover:bg-surface hover:text-primary", children: _jsx("svg", { viewBox: "0 0 16 16", width: "16", height: "16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 4l8 8M12 4l-8 8", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) })] }), children] })] }))] }));
|
|
90
|
-
}
|