nexus-public 1.0.12 → 3.0.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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/dist/Client.Index.d.ts +1 -0
  4. package/dist/Client.Index.js +3 -0
  5. package/dist/Components/Layouts/SiteBrandMark.d.ts +12 -0
  6. package/dist/Components/Layouts/SiteBrandMark.js +13 -0
  7. package/dist/Components/Layouts/SiteFooter.d.ts +3 -0
  8. package/dist/Components/Layouts/SiteFooter.js +9 -0
  9. package/dist/Components/Layouts/SiteHeader.d.ts +9 -0
  10. package/dist/Components/Layouts/SiteHeader.js +53 -0
  11. package/dist/Components/Layouts/SiteLayout.d.ts +17 -0
  12. package/dist/Components/Layouts/SiteLayout.js +15 -0
  13. package/dist/Components/Sections/FeatureCard.d.ts +12 -0
  14. package/dist/Components/Sections/FeatureCard.js +10 -0
  15. package/dist/Components/Sections/Hero.d.ts +17 -0
  16. package/dist/Components/Sections/Hero.js +6 -0
  17. package/dist/Components/Sections/ShowcaseCard.d.ts +25 -0
  18. package/dist/Components/Sections/ShowcaseCard.js +13 -0
  19. package/dist/Components/Sections/SiteSection.d.ts +16 -0
  20. package/dist/Components/Sections/SiteSection.js +7 -0
  21. package/dist/Helpers/SiteHelpers.d.ts +27 -0
  22. package/dist/Helpers/SiteHelpers.js +42 -0
  23. package/dist/Interfaces/SiteInterfaces.d.ts +59 -0
  24. package/dist/Interfaces/SiteInterfaces.js +1 -0
  25. package/dist/Server.Index.d.ts +1 -0
  26. package/dist/Server.Index.js +3 -0
  27. package/dist/Shared.Index.d.ts +9 -0
  28. package/dist/Shared.Index.js +14 -0
  29. package/package.json +60 -30
  30. package/src/Styles/Nexus.Public.Index.css +9 -0
  31. package/src/Styles/Nexus.Site.css +646 -0
  32. package/src/api-services/system-service.tsx +0 -13
  33. package/src/client.ts +0 -3
  34. package/src/components/layouts/client-layout.tsx +0 -32
  35. package/src/components/layouts/client-menu.tsx +0 -177
  36. package/src/index.ts +0 -1
  37. package/src/styles/nexus.layout.css +0 -24
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bijay Adhikari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # nexus-public
2
+
3
+ The public side of a Next.js app: the header with its menu, the footer, and the bands a page is built from — a hero,
4
+ sections, feature cards, and cards for the things a site lists. Built on
5
+ [`nexus-shared`](https://www.npmjs.com/package/nexus-shared), so it carries the same tokens and all seven themes in
6
+ light and dark.
7
+
8
+ ```bash
9
+ npm install nexus-public
10
+ ```
11
+
12
+ ## The shell
13
+
14
+ ```tsx
15
+ // app/(site)/layout.tsx — a server component
16
+ import { SiteLayout } from "nexus-public";
17
+ import type { SiteMenuItem } from "nexus-public";
18
+ import { ThemeSwitcher } from "nexus-shared/client";
19
+
20
+ const MENU: SiteMenuItem[] = [
21
+ { label: "Home", href: "/" },
22
+ {
23
+ label: "Rooms",
24
+ children: [
25
+ { label: "All rooms", href: "/rooms", icon: "bed", description: "Every room and suite." },
26
+ { label: "Suites", href: "/rooms#suites", icon: "star" },
27
+ ],
28
+ },
29
+ { label: "Contact", href: "/contact" },
30
+ ];
31
+
32
+ export default function Layout({ children }: { children: React.ReactNode }) {
33
+ return (
34
+ <SiteLayout
35
+ brand={{ name: "Nexus Reservations", tagline: "Stay well" }}
36
+ menu={MENU}
37
+ footer={{ brand: BRAND, description: "…", columns: FOOTER_COLUMNS }}
38
+ actions={<ThemeSwitcher />}
39
+ >
40
+ {children}
41
+ </SiteLayout>
42
+ );
43
+ }
44
+ ```
45
+
46
+ - Only the page in view is marked, so the site's home does not stay marked on every page under it.
47
+ - A menu entry with `children` opens a panel under it; the panel holds real links, so the first click navigates.
48
+ - Under 860px the menu moves into a sheet behind the ☰ button, and a navigation closes it.
49
+
50
+ ## A page
51
+
52
+ ```tsx
53
+ import { FeatureCard, Hero, ShowcaseCard, SiteSection } from "nexus-public";
54
+
55
+ export default function Home() {
56
+ return (
57
+ <>
58
+ <Hero eyebrow="Nexus Reservations" title="A room in Pokhara" lead="…" actions={<Link className="nx-btn nx-btn--primary nx-btn--lg" href="/rooms">See the rooms</Link>} aside={<StaySearch />} />
59
+ <SiteSection title="What every stay includes" lead="…">
60
+ <FeatureCard icon="wifi" title="Wi-Fi that works">Fibre in every room.</FeatureCard>
61
+ </SiteSection>
62
+ <SiteSection title="Rooms" action={<Link href="/rooms">All rooms</Link>}>
63
+ <ShowcaseCard title="Garden double" subtitle="Over the garden" meta="2 guests · 26 m²" price="NPR 5,400" note="4.6 ★" href="/rooms" />
64
+ </SiteSection>
65
+ </>
66
+ );
67
+ }
68
+ ```
69
+
70
+ | Export | What |
71
+ |---|---|
72
+ | `SiteLayout` | The page: header, the page itself, footer, filling the window |
73
+ | `SiteHeader` | The bar alone, for an app that arranges the rest itself (client) |
74
+ | `SiteFooter`, `SiteBrandMark` | The foot of the page, and the name and mark that link home |
75
+ | `Hero` | The first band: what the site is, and the one thing to do next. `aside` puts a card or a form beside it |
76
+ | `SiteSection` | A band with a heading and a line under it. `tone="band"` sets it apart from the ones around it |
77
+ | `FeatureCard` | An icon, a title, and a line or two |
78
+ | `ShowcaseCard` | A record on a page: a room, a place, an article, with a picture, facts, and a price |
79
+ | `currentHref`, `isCurrentLink`, `isCurrentParent`, `toHref` | What the menu uses to work out where the visitor is |
80
+
81
+ The button classes from `nexus-shared` work on a link, so a call to action can navigate:
82
+ `<Link className="nx-btn nx-btn--primary nx-btn--lg" href="…">`.
83
+
84
+ ## Styles
85
+
86
+ ```ts
87
+ import "nexus-shared/styles.css";
88
+ import "nexus-public/styles.css";
89
+ ```
90
+
91
+ Everything sits in the `nexus.components` cascade layer that `nexus-shared` declares, so styles an app writes outside
92
+ a layer win without `!important`.
93
+
94
+ ## Entry points
95
+
96
+ | Import | What |
97
+ |---|---|
98
+ | `nexus-public` | Everything that runs anywhere: the layout, the bands, the helpers, the types |
99
+ | `nexus-public/client` | `SiteHeader`, the one part that keeps state |
100
+ | `nexus-public/server` | Server-only code |
101
+ | `nexus-public/styles.css` | The stylesheet |
102
+
103
+ ## Peer dependencies
104
+
105
+ `next` >= 16, `react` >= 19, `react-dom` >= 19.
106
+
107
+ ## License
108
+
109
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ export * from "./Components/Layouts/SiteHeader.tsx";
@@ -0,0 +1,3 @@
1
+ // Client components: files here start with "use client".
2
+ // Import from "nexus-public/client".
3
+ export * from "./Components/Layouts/SiteHeader.js";
@@ -0,0 +1,12 @@
1
+ import type { SiteBrand } from "../../Interfaces/SiteInterfaces.ts";
2
+ export interface SiteBrandMarkProps {
3
+ brand: SiteBrand;
4
+ /** Default `md`. `sm` is the footer's size. */
5
+ size?: "sm" | "md";
6
+ className?: string;
7
+ }
8
+ /**
9
+ * The name and mark that link home, in the header and again in the footer. The first word is bold and the rest light,
10
+ * so "Nexus Reservations" reads as one name with an accent on the first word.
11
+ */
12
+ export declare function SiteBrandMark({ brand, size, className }: SiteBrandMarkProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from "nexus-icons";
3
+ import Link from "next/link";
4
+ import { toHref } from "../../Helpers/SiteHelpers.js";
5
+ /**
6
+ * The name and mark that link home, in the header and again in the footer. The first word is bold and the rest light,
7
+ * so "Nexus Reservations" reads as one name with an accent on the first word.
8
+ */
9
+ export function SiteBrandMark({ brand, size = "md", className }) {
10
+ const [first, ...rest] = brand.name.split(" ");
11
+ const classes = ["nx-brand", size === "sm" && "nx-brand--sm", className].filter(Boolean).join(" ");
12
+ return (_jsxs(Link, { href: toHref(brand.href ?? "/"), className: classes, "aria-label": `${brand.name} home`, children: [brand.logo ?? (_jsx("span", { className: "nx-brand__mark", "aria-hidden": "true", children: _jsx(Icon, { name: brand.icon ?? "hexagon-letter-n", size: size === "sm" ? 18 : 22, stroke: 1.75 }) })), _jsxs("span", { className: "nx-brand__text", children: [_jsxs("span", { className: "nx-brand__name", children: [_jsx("b", { children: first }), rest.length > 0 && _jsx("span", { className: "nx-brand__rest", children: ` ${rest.join(" ")}` })] }), brand.tagline && _jsx("small", { className: "nx-brand__tagline", children: brand.tagline })] })] }));
13
+ }
@@ -0,0 +1,3 @@
1
+ import type { SiteFooterProps } from "../../Interfaces/SiteInterfaces.ts";
2
+ /** The foot of every public page: the brand with a line about it, columns of links, and a bottom bar. */
3
+ export declare function SiteFooter({ brand, description, columns, copyright, bottom, className }: SiteFooterProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "nexus-shared";
3
+ import Link from "next/link";
4
+ import { externalLinkProps, toHref } from "../../Helpers/SiteHelpers.js";
5
+ import { SiteBrandMark } from "./SiteBrandMark.js";
6
+ /** The foot of every public page: the brand with a line about it, columns of links, and a bottom bar. */
7
+ export function SiteFooter({ brand, description, columns = [], copyright, bottom, className }) {
8
+ return (_jsxs("footer", { className: cx("nx-sitefoot", className), children: [_jsxs("div", { className: "nx-sitefoot__top", children: [_jsxs("div", { className: "nx-sitefoot__about", children: [_jsx(SiteBrandMark, { brand: brand, size: "sm" }), description && _jsx("p", { className: "nx-sitefoot__lead", children: description })] }), columns.map(column => (_jsxs("nav", { className: "nx-sitefoot__column", "aria-label": column.title, children: [_jsx("h2", { className: "nx-sitefoot__title", children: column.title }), column.links.map(link => (_jsx(Link, { href: toHref(link.href ?? "/"), ...(link.external ? externalLinkProps : {}), children: link.label }, link.label)))] }, column.title)))] }), _jsxs("div", { className: "nx-sitefoot__bottom", children: [_jsx("span", { children: copyright ?? `© ${new Date().getFullYear()} ${brand.name}` }), bottom && _jsx("span", { className: "nx-sitefoot__note", children: bottom })] })] }));
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { SiteHeaderProps } from "../../Interfaces/SiteInterfaces.ts";
2
+ /**
3
+ * The bar at the top of a public site: the brand, the menu, and the app's own actions on the right. A menu item with
4
+ * `children` opens a panel under it; on a phone the whole menu moves into a sheet behind the ☰ button.
5
+ *
6
+ * The panels are plain elements, not popovers: a site menu holds links, and a link must navigate on the first click,
7
+ * which a light-dismissed popover can swallow.
8
+ */
9
+ export declare function SiteHeader({ brand, menu, actions, sticky, className }: SiteHeaderProps): import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Icon, iconChevronDown, iconMenu2, iconX, InlineIcon } from "nexus-icons";
4
+ import { cx } from "nexus-shared";
5
+ import Link from "next/link";
6
+ import { usePathname } from "next/navigation";
7
+ import { useEffect, useRef, useState } from "react";
8
+ import { currentHref, externalLinkProps, isCurrentLink, isCurrentParent, toHref } from "../../Helpers/SiteHelpers.js";
9
+ import { SiteBrandMark } from "./SiteBrandMark.js";
10
+ /**
11
+ * The bar at the top of a public site: the brand, the menu, and the app's own actions on the right. A menu item with
12
+ * `children` opens a panel under it; on a phone the whole menu moves into a sheet behind the ☰ button.
13
+ *
14
+ * The panels are plain elements, not popovers: a site menu holds links, and a link must navigate on the first click,
15
+ * which a light-dismissed popover can swallow.
16
+ */
17
+ export function SiteHeader({ brand, menu = [], actions, sticky = true, className }) {
18
+ const pathname = usePathname();
19
+ const [panel, setPanel] = useState(null);
20
+ const [sheet, setSheet] = useState(false);
21
+ const navRef = useRef(null);
22
+ // The one entry the page belongs to: the longest path it is on or under.
23
+ const current = currentHref(menu, pathname);
24
+ // A navigation closes what the header had open, since the page under it changed.
25
+ useEffect(() => {
26
+ setPanel(null);
27
+ setSheet(false);
28
+ }, [pathname]);
29
+ // A press outside the menu, or Escape, closes an open panel.
30
+ useEffect(() => {
31
+ if (!panel)
32
+ return;
33
+ const close = (event) => {
34
+ if (!navRef.current?.contains(event.target))
35
+ setPanel(null);
36
+ };
37
+ const onKeyDown = (event) => {
38
+ if (event.key === "Escape")
39
+ setPanel(null);
40
+ };
41
+ document.addEventListener("pointerdown", close);
42
+ document.addEventListener("keydown", onKeyDown);
43
+ return () => {
44
+ document.removeEventListener("pointerdown", close);
45
+ document.removeEventListener("keydown", onKeyDown);
46
+ };
47
+ }, [panel]);
48
+ function itemLink(item, className) {
49
+ const marked = isCurrentLink(item.href, current);
50
+ return (_jsxs(Link, { href: toHref(item.href ?? "/"), className: cx(className, marked && "is-current"), "aria-current": marked ? "page" : undefined, ...(item.external ? externalLinkProps : {}), children: [item.icon && _jsx(Icon, { name: item.icon, size: 18 }), _jsx("span", { children: item.label })] }, item.label));
51
+ }
52
+ return (_jsxs("header", { className: cx("nx-site__header", sticky && "nx-site__header--sticky", className), children: [_jsxs("div", { className: "nx-site__bar", children: [_jsx(SiteBrandMark, { brand: brand }), _jsx("div", { className: "nx-sitenav", ref: navRef, children: _jsx("nav", { "aria-label": "Main", children: _jsx("ul", { className: "nx-sitenav__list", children: menu.map(item => (_jsx("li", { className: "nx-sitenav__item", children: item.children?.length ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: cx("nx-sitenav__link", isCurrentParent(item, current) && "is-current"), "aria-expanded": panel === item.label, onClick: () => setPanel(open => (open === item.label ? null : item.label)), children: [_jsx("span", { children: item.label }), _jsx(InlineIcon, { icon: iconChevronDown, className: "nx-sitenav__caret" })] }), panel === item.label && (_jsx("div", { className: "nx-sitenav__panel", children: item.children.map(child => (_jsxs(Link, { href: toHref(child.href ?? "/"), className: cx("nx-sitenav__child", isCurrentLink(child.href, current) && "is-current"), ...(child.external ? externalLinkProps : {}), children: [child.icon && _jsx(Icon, { name: child.icon, size: 20 }), _jsxs("span", { className: "nx-sitenav__child-text", children: [_jsx("span", { className: "nx-sitenav__child-label", children: child.label }), child.description && _jsx("small", { children: child.description })] })] }, child.label))) }))] })) : (itemLink(item, "nx-sitenav__link")) }, item.label))) }) }) }), _jsxs("div", { className: "nx-site__actions", children: [actions, menu.length > 0 && (_jsx("button", { type: "button", className: "nx-site__burger", "aria-label": sheet ? "Close the menu" : "Open the menu", "aria-expanded": sheet, "aria-controls": "nx-site-sheet", onClick: () => setSheet(open => !open), children: _jsx(InlineIcon, { icon: sheet ? iconX : iconMenu2, size: 20 }) }))] })] }), sheet && (_jsx("div", { className: "nx-site__sheet", id: "nx-site-sheet", children: _jsx("nav", { "aria-label": "Main, phone", children: menu.map(item => (_jsxs("div", { className: "nx-site__sheet-group", children: [item.href ? itemLink(item, "nx-site__sheet-link") : _jsx("p", { className: "nx-site__sheet-title", children: item.label }), item.children?.map(child => itemLink(child, "nx-site__sheet-link nx-site__sheet-link--child"))] }, item.label))) }) }))] }));
53
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SiteFooterProps, SiteHeaderProps } from "../../Interfaces/SiteInterfaces.ts";
3
+ export interface SiteLayoutProps extends SiteHeaderProps {
4
+ /** The footer's own content. Left out, it is the brand and the year; `false` leaves the footer off. */
5
+ footer?: SiteFooterProps | false;
6
+ children: ReactNode;
7
+ }
8
+ /**
9
+ * A public site's page: the header, the page itself, and the footer, in a column that fills the window even when
10
+ * the page is short. Give it to a Next.js layout, and every page under that layout gets the shell.
11
+ *
12
+ * @example
13
+ * export default function Layout({ children }) {
14
+ * return <SiteLayout brand={BRAND} menu={MENU} footer={FOOTER} actions={<ThemeSwitcher />}>{children}</SiteLayout>;
15
+ * }
16
+ */
17
+ export declare function SiteLayout({ children, footer, ...header }: SiteLayoutProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SiteFooter } from "./SiteFooter.js";
3
+ import { SiteHeader } from "./SiteHeader.js";
4
+ /**
5
+ * A public site's page: the header, the page itself, and the footer, in a column that fills the window even when
6
+ * the page is short. Give it to a Next.js layout, and every page under that layout gets the shell.
7
+ *
8
+ * @example
9
+ * export default function Layout({ children }) {
10
+ * return <SiteLayout brand={BRAND} menu={MENU} footer={FOOTER} actions={<ThemeSwitcher />}>{children}</SiteLayout>;
11
+ * }
12
+ */
13
+ export function SiteLayout({ children, footer, ...header }) {
14
+ return (_jsxs("div", { className: "nx-site", children: [_jsx(SiteHeader, { ...header }), _jsx("main", { className: "nx-site__main", id: "nx-site-main", children: children }), footer !== false && _jsx(SiteFooter, { ...(footer ?? { brand: header.brand }) })] }));
15
+ }
@@ -0,0 +1,12 @@
1
+ import { type IconName } from "nexus-icons";
2
+ import type { ReactNode } from "react";
3
+ export interface FeatureCardProps {
4
+ icon?: IconName;
5
+ title: ReactNode;
6
+ children?: ReactNode;
7
+ /** Turns the whole card into a link. */
8
+ href?: string;
9
+ className?: string;
10
+ }
11
+ /** One of the cards that say what a site offers: an icon, a title, and a line or two. */
12
+ export declare function FeatureCard({ icon, title, children, href, className }: FeatureCardProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Icon } from "nexus-icons";
3
+ import { cx } from "nexus-shared";
4
+ import Link from "next/link";
5
+ import { toHref } from "../../Helpers/SiteHelpers.js";
6
+ /** One of the cards that say what a site offers: an icon, a title, and a line or two. */
7
+ export function FeatureCard({ icon, title, children, href, className }) {
8
+ const body = (_jsxs(_Fragment, { children: [icon && (_jsx("span", { className: "nx-feature__icon", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: 22, stroke: 1.75 }) })), _jsx("h3", { className: "nx-feature__title", children: title }), children && _jsx("div", { className: "nx-feature__text", children: children })] }));
9
+ return href ? (_jsx(Link, { href: toHref(href), className: cx("nx-feature", "nx-feature--link", className), children: body })) : (_jsx("article", { className: cx("nx-feature", className), children: body }));
10
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+ export interface HeroProps {
3
+ /** A small line over the title, e.g. "Nexus Reservations". */
4
+ eyebrow?: ReactNode;
5
+ title: ReactNode;
6
+ /** A sentence or two under the title. */
7
+ lead?: ReactNode;
8
+ /** Buttons or links under the lead. */
9
+ actions?: ReactNode;
10
+ /** A search bar, a form, or a card beside the text. Without it the hero is one centered column. */
11
+ aside?: ReactNode;
12
+ /** Small facts under the text, e.g. "120 places · 12 cities". */
13
+ meta?: ReactNode;
14
+ className?: string;
15
+ }
16
+ /** The first band of a public home page: what the site is, and the one thing to do next. */
17
+ export declare function Hero({ eyebrow, title, lead, actions, aside, meta, className }: HeroProps): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "nexus-shared";
3
+ /** The first band of a public home page: what the site is, and the one thing to do next. */
4
+ export function Hero({ eyebrow, title, lead, actions, aside, meta, className }) {
5
+ return (_jsxs("section", { className: cx("nx-hero", aside ? "nx-hero--split" : "nx-hero--center", className), children: [_jsxs("div", { className: "nx-hero__text", children: [eyebrow && _jsx("p", { className: "nx-hero__eyebrow", children: eyebrow }), _jsx("h1", { className: "nx-hero__title", children: title }), lead && _jsx("p", { className: "nx-hero__lead", children: lead }), actions && _jsx("div", { className: "nx-hero__actions", children: actions }), meta && _jsx("div", { className: "nx-hero__meta", children: meta })] }), aside && _jsx("div", { className: "nx-hero__aside", children: aside })] }));
6
+ }
@@ -0,0 +1,25 @@
1
+ import { type IconName } from "nexus-icons";
2
+ import type { ReactNode } from "react";
3
+ export interface ShowcaseCardProps {
4
+ title: ReactNode;
5
+ /** Where it is, what it is: one muted line under the title. */
6
+ subtitle?: ReactNode;
7
+ /** The picture. Without one the card draws a tinted panel with `icon` in it. */
8
+ image?: ReactNode;
9
+ icon?: IconName;
10
+ /** A word over the picture, e.g. "Popular". */
11
+ badge?: ReactNode;
12
+ /** Short facts under the title: "2 guests · 28 m² · Breakfast". */
13
+ meta?: ReactNode;
14
+ /** The right of the bottom line, e.g. a price. */
15
+ price?: ReactNode;
16
+ /** The left of the bottom line, e.g. a rating. */
17
+ note?: ReactNode;
18
+ href?: string;
19
+ className?: string;
20
+ }
21
+ /**
22
+ * A record on a public page: a room, a place, an article. The picture is the app's, so the card takes a node and
23
+ * never loads anything itself; without one it draws a tinted panel with an icon, which is what the demos use.
24
+ */
25
+ export declare function ShowcaseCard({ title, subtitle, image, icon, badge, meta, price, note, href, className }: ShowcaseCardProps): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Icon } from "nexus-icons";
3
+ import { cx } from "nexus-shared";
4
+ import Link from "next/link";
5
+ import { toHref } from "../../Helpers/SiteHelpers.js";
6
+ /**
7
+ * A record on a public page: a room, a place, an article. The picture is the app's, so the card takes a node and
8
+ * never loads anything itself; without one it draws a tinted panel with an icon, which is what the demos use.
9
+ */
10
+ export function ShowcaseCard({ title, subtitle, image, icon = "photo", badge, meta, price, note, href, className }) {
11
+ const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "nx-showcase__media", children: [image ?? (_jsx("span", { className: "nx-showcase__placeholder", "aria-hidden": "true", children: _jsx(Icon, { name: icon, size: 28, stroke: 1.5 }) })), badge && _jsx("span", { className: "nx-showcase__badge", children: badge })] }), _jsxs("div", { className: "nx-showcase__body", children: [_jsx("h3", { className: "nx-showcase__title", children: title }), subtitle && _jsx("p", { className: "nx-showcase__subtitle", children: subtitle }), meta && _jsx("p", { className: "nx-showcase__meta", children: meta }), (note || price) && (_jsxs("p", { className: "nx-showcase__line", children: [_jsx("span", { className: "nx-showcase__note", children: note }), price && _jsx("span", { className: "nx-showcase__price", children: price })] }))] })] }));
12
+ return href ? (_jsx(Link, { href: toHref(href), className: cx("nx-showcase", "nx-showcase--link", className), children: body })) : (_jsx("article", { className: cx("nx-showcase", className), children: body }));
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ export interface SiteSectionProps {
3
+ /** Used by the heading's id, so the section can be linked to and named for screen readers. */
4
+ id?: string;
5
+ title?: ReactNode;
6
+ /** A line under the title. */
7
+ lead?: ReactNode;
8
+ /** A link or button on the right of the heading, e.g. "See all rooms". */
9
+ action?: ReactNode;
10
+ /** `band` draws the section on the page background, to set it apart from the ones around it. */
11
+ tone?: "plain" | "band";
12
+ children: ReactNode;
13
+ className?: string;
14
+ }
15
+ /** A band of a public page: a heading, a line under it, and the content. */
16
+ export declare function SiteSection({ id, title, lead, action, tone, children, className }: SiteSectionProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "nexus-shared";
3
+ /** A band of a public page: a heading, a line under it, and the content. */
4
+ export function SiteSection({ id, title, lead, action, tone = "plain", children, className }) {
5
+ const headingId = id ? `${id}-title` : undefined;
6
+ return (_jsxs("section", { className: cx("nx-site-section", tone === "band" && "nx-site-section--band", className), id: id, "aria-labelledby": title ? headingId : undefined, children: [(title || action) && (_jsxs("header", { className: "nx-site-section__head", children: [_jsxs("div", { children: [title && (_jsx("h2", { className: "nx-site-section__title", id: headingId, children: title })), lead && _jsx("p", { className: "nx-site-section__lead", children: lead })] }), action] })), children] }));
7
+ }
@@ -0,0 +1,27 @@
1
+ import type Link from "next/link";
2
+ import type { ComponentProps } from "react";
3
+ import type { SiteMenuItem } from "../Interfaces/SiteInterfaces.ts";
4
+ /** The type `next/link` gives `href`: a plain string, or one of the app's routes when `typedRoutes` is on. */
5
+ type LinkHref = ComponentProps<typeof Link>["href"];
6
+ /**
7
+ * A path from configuration, as `Link` takes it. An app with `typedRoutes` narrows `href` to the routes it has, which
8
+ * a package cannot know: menus take plain strings, and every link passes its path through here.
9
+ */
10
+ export declare function toHref(href: string): LinkHref;
11
+ /** True while the page is this link's, or one under it: `/rooms` also matches `/rooms/deluxe`. */
12
+ export declare function isUnderPath(href: string | undefined, pathname: string): boolean;
13
+ /**
14
+ * The one path in the menu that fits the page: the longest of those the page is on or under. Only that entry is
15
+ * marked, so a site's own home ("/site") does not stay marked while the visitor reads a page inside it.
16
+ */
17
+ export declare function currentHref(menu: readonly SiteMenuItem[], pathname: string): string | undefined;
18
+ /** True for the entry the page belongs to, as `currentHref` worked it out. */
19
+ export declare function isCurrentLink(href: string | undefined, current: string | undefined): boolean;
20
+ /** True while one of the parent's own entries is the page: the parent stays marked with its panel shut. */
21
+ export declare function isCurrentParent(item: SiteMenuItem, current: string | undefined): boolean;
22
+ /** What an external link needs, so a new tab cannot reach back into this one. */
23
+ export declare const externalLinkProps: {
24
+ readonly target: "_blank";
25
+ readonly rel: "noreferrer noopener";
26
+ };
27
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A path from configuration, as `Link` takes it. An app with `typedRoutes` narrows `href` to the routes it has, which
3
+ * a package cannot know: menus take plain strings, and every link passes its path through here.
4
+ */
5
+ export function toHref(href) {
6
+ return href;
7
+ }
8
+ /** True while the page is this link's, or one under it: `/rooms` also matches `/rooms/deluxe`. */
9
+ export function isUnderPath(href, pathname) {
10
+ if (!href || href.startsWith("http") || href.startsWith("#"))
11
+ return false;
12
+ if (href === "/")
13
+ return pathname === "/";
14
+ return pathname === href || pathname.startsWith(`${href}/`);
15
+ }
16
+ /**
17
+ * The one path in the menu that fits the page: the longest of those the page is on or under. Only that entry is
18
+ * marked, so a site's own home ("/site") does not stay marked while the visitor reads a page inside it.
19
+ */
20
+ export function currentHref(menu, pathname) {
21
+ let best;
22
+ const walk = (items) => {
23
+ for (const item of items) {
24
+ if (item.href && isUnderPath(item.href, pathname) && (best === undefined || item.href.length > best.length))
25
+ best = item.href;
26
+ if (item.children)
27
+ walk(item.children);
28
+ }
29
+ };
30
+ walk(menu);
31
+ return best;
32
+ }
33
+ /** True for the entry the page belongs to, as `currentHref` worked it out. */
34
+ export function isCurrentLink(href, current) {
35
+ return href !== undefined && href === current;
36
+ }
37
+ /** True while one of the parent's own entries is the page: the parent stays marked with its panel shut. */
38
+ export function isCurrentParent(item, current) {
39
+ return (item.children ?? []).some(child => isCurrentLink(child.href, current));
40
+ }
41
+ /** What an external link needs, so a new tab cannot reach back into this one. */
42
+ export const externalLinkProps = { target: "_blank", rel: "noreferrer noopener" };
@@ -0,0 +1,59 @@
1
+ import type { IconName } from "nexus-icons";
2
+ import type { ReactNode } from "react";
3
+ /**
4
+ * One entry of a public site's menu: a link, or a parent that opens a panel of links under it.
5
+ * The site layout takes a plain array of these, so a menu can be a constant in the app, or built from the API.
6
+ */
7
+ export interface SiteMenuItem {
8
+ label: string;
9
+ /** Path or URL. A parent with `children` and no `href` only opens its panel. */
10
+ href?: string;
11
+ /** Tabler icon, shown in the panel under a parent and in the phone menu. */
12
+ icon?: IconName;
13
+ /** A second line in muted text, in the panel under a parent. */
14
+ description?: string;
15
+ /** Turns the entry into a parent: the header shows a panel under it, the phone menu a group. */
16
+ children?: SiteMenuItem[];
17
+ /** Opens in a new tab, with the usual `rel`. */
18
+ external?: boolean;
19
+ }
20
+ /** The name and mark in the top left, which link home. */
21
+ export interface SiteBrand {
22
+ /** The first word is drawn bold, the rest light, as on the old Nexus sites. */
23
+ name: string;
24
+ /** A small second line under the name, e.g. "RESERVATIONS". */
25
+ tagline?: string;
26
+ /** Default "/". */
27
+ href?: string;
28
+ /** Tabler icon in the mark. Default `hexagon-letter-n`. */
29
+ icon?: IconName;
30
+ /** Drawn in place of the mark and the name, for a real logo. */
31
+ logo?: ReactNode;
32
+ }
33
+ /** One column of links in the footer. */
34
+ export interface SiteFooterColumn {
35
+ title: string;
36
+ links: SiteMenuItem[];
37
+ }
38
+ /** A site's footer: the brand with a line about it, columns of links, and a bottom bar. */
39
+ export interface SiteFooterProps {
40
+ brand: SiteBrand;
41
+ /** A sentence or two under the brand. */
42
+ description?: ReactNode;
43
+ columns?: SiteFooterColumn[];
44
+ /** Small print on the left of the bottom bar. Default "© <year> <brand name>". */
45
+ copyright?: ReactNode;
46
+ /** The right of the bottom bar: links, a note, a language picker. */
47
+ bottom?: ReactNode;
48
+ className?: string;
49
+ }
50
+ /** The bar at the top of every public page. */
51
+ export interface SiteHeaderProps {
52
+ brand: SiteBrand;
53
+ menu?: SiteMenuItem[];
54
+ /** The right of the bar: a theme switcher, Sign in, a call to action. */
55
+ actions?: ReactNode;
56
+ /** Sticks to the top of the window while the page scrolls. Default true. */
57
+ sticky?: boolean;
58
+ className?: string;
59
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ // Server-only code, such as layouts that read configuration.
2
+ // Import from "nexus-public/server".
3
+ export {};
@@ -0,0 +1,9 @@
1
+ export * from "./Components/Layouts/SiteBrandMark.tsx";
2
+ export * from "./Components/Layouts/SiteFooter.tsx";
3
+ export * from "./Components/Layouts/SiteLayout.tsx";
4
+ export * from "./Components/Sections/FeatureCard.tsx";
5
+ export * from "./Components/Sections/Hero.tsx";
6
+ export * from "./Components/Sections/ShowcaseCard.tsx";
7
+ export * from "./Components/Sections/SiteSection.tsx";
8
+ export * from "./Helpers/SiteHelpers.ts";
9
+ export * from "./Interfaces/SiteInterfaces.ts";
@@ -0,0 +1,14 @@
1
+ // Works everywhere: server components, client components, and plain TypeScript.
2
+ // The layout and the page bands are server components; the header inside them is the one client part.
3
+ // Components
4
+ export * from "./Components/Layouts/SiteBrandMark.js";
5
+ export * from "./Components/Layouts/SiteFooter.js";
6
+ export * from "./Components/Layouts/SiteLayout.js";
7
+ export * from "./Components/Sections/FeatureCard.js";
8
+ export * from "./Components/Sections/Hero.js";
9
+ export * from "./Components/Sections/ShowcaseCard.js";
10
+ export * from "./Components/Sections/SiteSection.js";
11
+ // Helpers
12
+ export * from "./Helpers/SiteHelpers.js";
13
+ // Interfaces
14
+ export * from "./Interfaces/SiteInterfaces.js";