@nexpress/theme-default 0.1.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/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/components/dark-mode-toggle.d.ts +5 -0
- package/dist/components/dark-mode-toggle.js +101 -0
- package/dist/components/dark-mode-toggle.js.map +1 -0
- package/dist/components/footer-columns.d.ts +17 -0
- package/dist/components/footer-columns.js +156 -0
- package/dist/components/footer-columns.js.map +1 -0
- package/dist/components/language-picker.d.ts +39 -0
- package/dist/components/language-picker.js +52 -0
- package/dist/components/language-picker.js.map +1 -0
- package/dist/components/member-status-widget.d.ts +17 -0
- package/dist/components/member-status-widget.js +78 -0
- package/dist/components/member-status-widget.js.map +1 -0
- package/dist/components/mobile-nav.d.ts +23 -0
- package/dist/components/mobile-nav.js +120 -0
- package/dist/components/mobile-nav.js.map +1 -0
- package/dist/components/newsletter-form.d.ts +17 -0
- package/dist/components/newsletter-form.js +74 -0
- package/dist/components/newsletter-form.js.map +1 -0
- package/dist/index.d.ts +192 -0
- package/dist/index.js +1388 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/components/mobile-nav.tsx
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
function MobileNav({ items, label = "Menu" }) {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!open) return;
|
|
12
|
+
const onKey = (e) => {
|
|
13
|
+
if (e.key === "Escape") setOpen(false);
|
|
14
|
+
};
|
|
15
|
+
document.addEventListener("keydown", onKey);
|
|
16
|
+
const previousOverflow = document.body.style.overflow;
|
|
17
|
+
document.body.style.overflow = "hidden";
|
|
18
|
+
return () => {
|
|
19
|
+
document.removeEventListener("keydown", onKey);
|
|
20
|
+
document.body.style.overflow = previousOverflow;
|
|
21
|
+
};
|
|
22
|
+
}, [open]);
|
|
23
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
24
|
+
/* @__PURE__ */ jsx(
|
|
25
|
+
"button",
|
|
26
|
+
{
|
|
27
|
+
type: "button",
|
|
28
|
+
className: "np-mobile-nav-toggle",
|
|
29
|
+
"aria-label": open ? "Close menu" : "Open menu",
|
|
30
|
+
"aria-expanded": open,
|
|
31
|
+
"aria-controls": "np-mobile-nav-drawer",
|
|
32
|
+
onClick: () => setOpen((prev) => !prev),
|
|
33
|
+
children: open ? /* @__PURE__ */ jsx(CloseIcon, {}) : /* @__PURE__ */ jsx(MenuIcon, {})
|
|
34
|
+
}
|
|
35
|
+
),
|
|
36
|
+
open ? /* @__PURE__ */ jsx(
|
|
37
|
+
"div",
|
|
38
|
+
{
|
|
39
|
+
className: "np-mobile-nav-overlay",
|
|
40
|
+
role: "presentation",
|
|
41
|
+
onClick: () => setOpen(false)
|
|
42
|
+
}
|
|
43
|
+
) : null,
|
|
44
|
+
/* @__PURE__ */ jsxs(
|
|
45
|
+
"aside",
|
|
46
|
+
{
|
|
47
|
+
id: "np-mobile-nav-drawer",
|
|
48
|
+
className: "np-mobile-nav-drawer",
|
|
49
|
+
"data-open": open ? "true" : "false",
|
|
50
|
+
"aria-hidden": open ? "false" : "true",
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsxs("header", { className: "np-mobile-nav-drawer-header", children: [
|
|
53
|
+
/* @__PURE__ */ jsx("span", { className: "np-mobile-nav-drawer-label", children: label }),
|
|
54
|
+
/* @__PURE__ */ jsx(
|
|
55
|
+
"button",
|
|
56
|
+
{
|
|
57
|
+
type: "button",
|
|
58
|
+
className: "np-mobile-nav-close",
|
|
59
|
+
onClick: () => setOpen(false),
|
|
60
|
+
"aria-label": "Close menu",
|
|
61
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
] }),
|
|
65
|
+
/* @__PURE__ */ jsx("ul", { className: "np-mobile-nav-list", children: items.map((item, index) => /* @__PURE__ */ jsxs("li", { children: [
|
|
66
|
+
item.url ? /* @__PURE__ */ jsx(Link, { href: item.url, onClick: () => setOpen(false), children: item.label }) : /* @__PURE__ */ jsx("span", { children: item.label }),
|
|
67
|
+
item.children && item.children.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "np-mobile-subnav", children: item.children.map((child, childIndex) => /* @__PURE__ */ jsx("li", { children: child.url ? /* @__PURE__ */ jsx(Link, { href: child.url, onClick: () => setOpen(false), children: child.label }) : /* @__PURE__ */ jsx("span", { children: child.label }) }, `mobile-nav-${index.toString()}-${childIndex.toString()}`)) }) : null
|
|
68
|
+
] }, `mobile-nav-${index.toString()}`)) })
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
] });
|
|
73
|
+
}
|
|
74
|
+
function MenuIcon() {
|
|
75
|
+
return /* @__PURE__ */ jsxs(
|
|
76
|
+
"svg",
|
|
77
|
+
{
|
|
78
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
79
|
+
width: "20",
|
|
80
|
+
height: "20",
|
|
81
|
+
viewBox: "0 0 24 24",
|
|
82
|
+
fill: "none",
|
|
83
|
+
stroke: "currentColor",
|
|
84
|
+
strokeWidth: "2",
|
|
85
|
+
strokeLinecap: "round",
|
|
86
|
+
strokeLinejoin: "round",
|
|
87
|
+
"aria-hidden": "true",
|
|
88
|
+
children: [
|
|
89
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
90
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
91
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function CloseIcon() {
|
|
97
|
+
return /* @__PURE__ */ jsxs(
|
|
98
|
+
"svg",
|
|
99
|
+
{
|
|
100
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
101
|
+
width: "20",
|
|
102
|
+
height: "20",
|
|
103
|
+
viewBox: "0 0 24 24",
|
|
104
|
+
fill: "none",
|
|
105
|
+
stroke: "currentColor",
|
|
106
|
+
strokeWidth: "2",
|
|
107
|
+
strokeLinecap: "round",
|
|
108
|
+
strokeLinejoin: "round",
|
|
109
|
+
"aria-hidden": "true",
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
112
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "18", x2: "18", y2: "6" })
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
MobileNav
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=mobile-nav.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/mobile-nav.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport Link from \"next/link\";\nimport type { NpNavItem } from \"@nexpress/core\";\n\n/**\n * Mobile-first nav drawer. The desktop header keeps its inline\n * link list visible above ~768px (CSS handles the hide/show);\n * below that breakpoint the inline nav is hidden by CSS and the\n * hamburger button + slide-in drawer take over.\n *\n * Why a client component: the drawer needs `useState` for\n * open/closed, focus-trap on Escape, and scroll-lock on the\n * body. The link list itself is passed in from the server-\n * rendered header so the markup stays SEO-visible even when\n * the drawer is closed.\n */\nexport interface MobileNavProps {\n items: NpNavItem[];\n /** Optional brand label for the drawer header. Defaults to \"Menu\". */\n label?: string;\n}\n\nexport function MobileNav({ items, label = \"Menu\" }: MobileNavProps) {\n const [open, setOpen] = useState(false);\n\n // Close on Escape and lock body scroll while open. Both effects\n // run only when the drawer is actually open so no listeners\n // hang around in the closed state.\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"keydown\", onKey);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n return (\n <>\n <button\n type=\"button\"\n className=\"np-mobile-nav-toggle\"\n aria-label={open ? \"Close menu\" : \"Open menu\"}\n aria-expanded={open}\n aria-controls=\"np-mobile-nav-drawer\"\n onClick={() => setOpen((prev) => !prev)}\n >\n {open ? <CloseIcon /> : <MenuIcon />}\n </button>\n {open ? (\n <div\n className=\"np-mobile-nav-overlay\"\n role=\"presentation\"\n onClick={() => setOpen(false)}\n />\n ) : null}\n <aside\n id=\"np-mobile-nav-drawer\"\n className=\"np-mobile-nav-drawer\"\n data-open={open ? \"true\" : \"false\"}\n aria-hidden={open ? \"false\" : \"true\"}\n >\n <header className=\"np-mobile-nav-drawer-header\">\n <span className=\"np-mobile-nav-drawer-label\">{label}</span>\n <button\n type=\"button\"\n className=\"np-mobile-nav-close\"\n onClick={() => setOpen(false)}\n aria-label=\"Close menu\"\n >\n <CloseIcon />\n </button>\n </header>\n <ul className=\"np-mobile-nav-list\">\n {items.map((item, index) => (\n <li key={`mobile-nav-${index.toString()}`}>\n {item.url ? (\n <Link href={item.url} onClick={() => setOpen(false)}>\n {item.label}\n </Link>\n ) : (\n <span>{item.label}</span>\n )}\n {item.children && item.children.length > 0 ? (\n <ul className=\"np-mobile-subnav\">\n {item.children.map((child, childIndex) => (\n <li key={`mobile-nav-${index.toString()}-${childIndex.toString()}`}>\n {child.url ? (\n <Link href={child.url} onClick={() => setOpen(false)}>\n {child.label}\n </Link>\n ) : (\n <span>{child.label}</span>\n )}\n </li>\n ))}\n </ul>\n ) : null}\n </li>\n ))}\n </ul>\n </aside>\n </>\n );\n}\n\nfunction MenuIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <line x1=\"4\" y1=\"6\" x2=\"20\" y2=\"6\" />\n <line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\" />\n <line x1=\"4\" y1=\"18\" x2=\"20\" y2=\"18\" />\n </svg>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n <line x1=\"6\" y1=\"18\" x2=\"18\" y2=\"6\" />\n </svg>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight newsletter form. Submits to `/api/newsletter`
|
|
5
|
+
* (operators wire this up in their app, or hand it off to a
|
|
6
|
+
* provider like ConvertKit / Buttondown via a small route
|
|
7
|
+
* handler). When the endpoint isn't present we degrade
|
|
8
|
+
* gracefully — the user sees an "endpoint not configured"
|
|
9
|
+
* notice rather than a stack trace.
|
|
10
|
+
*
|
|
11
|
+
* Optimistic UX: the input is replaced by a "thanks" message
|
|
12
|
+
* the moment the response is OK. Errors keep the input visible
|
|
13
|
+
* so the user can retry.
|
|
14
|
+
*/
|
|
15
|
+
declare function NewsletterForm(): react_jsx_runtime.JSX.Element;
|
|
16
|
+
|
|
17
|
+
export { NewsletterForm };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/components/newsletter-form.tsx
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function NewsletterForm() {
|
|
8
|
+
const [email, setEmail] = useState("");
|
|
9
|
+
const [state, setState] = useState({ kind: "idle" });
|
|
10
|
+
async function onSubmit(e) {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
if (state.kind === "submitting") return;
|
|
13
|
+
setState({ kind: "submitting" });
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch("/api/newsletter", {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "content-type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ email })
|
|
19
|
+
});
|
|
20
|
+
if (res.status === 404) {
|
|
21
|
+
setState({
|
|
22
|
+
kind: "error",
|
|
23
|
+
message: "Newsletter endpoint not configured."
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const body = await res.json().catch(() => null);
|
|
29
|
+
setState({
|
|
30
|
+
kind: "error",
|
|
31
|
+
message: body?.error?.message ?? "Subscription failed. Try again."
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
setState({ kind: "ok" });
|
|
36
|
+
setEmail("");
|
|
37
|
+
} catch {
|
|
38
|
+
setState({ kind: "error", message: "Network error." });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (state.kind === "ok") {
|
|
42
|
+
return /* @__PURE__ */ jsx("p", { className: "np-site-footer-subscribe-success", role: "status", children: "Thanks \u2014 you're subscribed." });
|
|
43
|
+
}
|
|
44
|
+
return /* @__PURE__ */ jsxs(
|
|
45
|
+
"form",
|
|
46
|
+
{
|
|
47
|
+
className: "np-site-footer-subscribe-form",
|
|
48
|
+
onSubmit: (e) => {
|
|
49
|
+
void onSubmit(e);
|
|
50
|
+
},
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ jsx("label", { className: "sr-only", htmlFor: "np-newsletter-email", children: "Email address" }),
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"input",
|
|
55
|
+
{
|
|
56
|
+
id: "np-newsletter-email",
|
|
57
|
+
type: "email",
|
|
58
|
+
required: true,
|
|
59
|
+
autoComplete: "email",
|
|
60
|
+
placeholder: "you@example.com",
|
|
61
|
+
value: email,
|
|
62
|
+
onChange: (e) => setEmail(e.target.value)
|
|
63
|
+
}
|
|
64
|
+
),
|
|
65
|
+
/* @__PURE__ */ jsx("button", { type: "submit", disabled: state.kind === "submitting", children: state.kind === "submitting" ? "Subscribing\u2026" : "Subscribe" }),
|
|
66
|
+
state.kind === "error" ? /* @__PURE__ */ jsx("p", { className: "np-site-footer-subscribe-error", role: "alert", children: state.message }) : null
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
NewsletterForm
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=newsletter-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/newsletter-form.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\n\n/**\n * Lightweight newsletter form. Submits to `/api/newsletter`\n * (operators wire this up in their app, or hand it off to a\n * provider like ConvertKit / Buttondown via a small route\n * handler). When the endpoint isn't present we degrade\n * gracefully — the user sees an \"endpoint not configured\"\n * notice rather than a stack trace.\n *\n * Optimistic UX: the input is replaced by a \"thanks\" message\n * the moment the response is OK. Errors keep the input visible\n * so the user can retry.\n */\nexport function NewsletterForm() {\n const [email, setEmail] = useState(\"\");\n const [state, setState] = useState<\n | { kind: \"idle\" }\n | { kind: \"submitting\" }\n | { kind: \"ok\" }\n | { kind: \"error\"; message: string }\n >({ kind: \"idle\" });\n\n async function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault();\n if (state.kind === \"submitting\") return;\n setState({ kind: \"submitting\" });\n try {\n const res = await fetch(\"/api/newsletter\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ email }),\n });\n if (res.status === 404) {\n setState({\n kind: \"error\",\n message: \"Newsletter endpoint not configured.\",\n });\n return;\n }\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as\n | { error?: { message?: string } }\n | null;\n setState({\n kind: \"error\",\n message: body?.error?.message ?? \"Subscription failed. Try again.\",\n });\n return;\n }\n setState({ kind: \"ok\" });\n setEmail(\"\");\n } catch {\n setState({ kind: \"error\", message: \"Network error.\" });\n }\n }\n\n if (state.kind === \"ok\") {\n return (\n <p className=\"np-site-footer-subscribe-success\" role=\"status\">\n Thanks — you're subscribed.\n </p>\n );\n }\n\n return (\n <form\n className=\"np-site-footer-subscribe-form\"\n onSubmit={(e) => {\n void onSubmit(e);\n }}\n >\n <label className=\"sr-only\" htmlFor=\"np-newsletter-email\">\n Email address\n </label>\n <input\n id=\"np-newsletter-email\"\n type=\"email\"\n required\n autoComplete=\"email\"\n placeholder=\"you@example.com\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n <button type=\"submit\" disabled={state.kind === \"submitting\"}>\n {state.kind === \"submitting\" ? \"Subscribing…\" : \"Subscribe\"}\n </button>\n {state.kind === \"error\" ? (\n <p className=\"np-site-footer-subscribe-error\" role=\"alert\">\n {state.message}\n </p>\n ) : null}\n </form>\n );\n}\n"],"mappings":";;;;AAEA,SAAS,gBAAgB;AA2DnB,cAOF,YAPE;AA7CC,SAAS,iBAAiB;AAC/B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAKxB,EAAE,MAAM,OAAO,CAAC;AAElB,iBAAe,SAAS,GAAqC;AAC3D,MAAE,eAAe;AACjB,QAAI,MAAM,SAAS,aAAc;AACjC,aAAS,EAAE,MAAM,aAAa,CAAC;AAC/B,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,mBAAmB;AAAA,QACzC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC,CAAC;AACD,UAAI,IAAI,WAAW,KAAK;AACtB,iBAAS;AAAA,UACP,MAAM;AAAA,UACN,SAAS;AAAA,QACX,CAAC;AACD;AAAA,MACF;AACA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI;AAG/C,iBAAS;AAAA,UACP,MAAM;AAAA,UACN,SAAS,MAAM,OAAO,WAAW;AAAA,QACnC,CAAC;AACD;AAAA,MACF;AACA,eAAS,EAAE,MAAM,KAAK,CAAC;AACvB,eAAS,EAAE;AAAA,IACb,QAAQ;AACN,eAAS,EAAE,MAAM,SAAS,SAAS,iBAAiB,CAAC;AAAA,IACvD;AAAA,EACF;AAEA,MAAI,MAAM,SAAS,MAAM;AACvB,WACE,oBAAC,OAAE,WAAU,oCAAmC,MAAK,UAAS,8CAE9D;AAAA,EAEJ;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,UAAU,CAAC,MAAM;AACf,aAAK,SAAS,CAAC;AAAA,MACjB;AAAA,MAEA;AAAA,4BAAC,WAAM,WAAU,WAAU,SAAQ,uBAAsB,2BAEzD;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,MAAK;AAAA,YACL,UAAQ;AAAA,YACR,cAAa;AAAA,YACb,aAAY;AAAA,YACZ,OAAO;AAAA,YACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA;AAAA,QAC1C;AAAA,QACA,oBAAC,YAAO,MAAK,UAAS,UAAU,MAAM,SAAS,cAC5C,gBAAM,SAAS,eAAe,sBAAiB,aAClD;AAAA,QACC,MAAM,SAAS,UACd,oBAAC,OAAE,WAAU,kCAAiC,MAAK,SAChD,gBAAM,SACT,IACE;AAAA;AAAA;AAAA,EACN;AAEJ;","names":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as _nexpress_theme from '@nexpress/theme';
|
|
2
|
+
import { NpThemeShellProps, NpTemplateRenderProps } from '@nexpress/theme';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
5
|
+
export { DefaultFooter } from './components/footer-columns.js';
|
|
6
|
+
export { MemberStatusWidget } from './components/member-status-widget.js';
|
|
7
|
+
export { MobileNav } from './components/mobile-nav.js';
|
|
8
|
+
export { NewsletterForm } from './components/newsletter-form.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default theme shell — wraps every (site) route. The shell
|
|
12
|
+
* also owns this theme's color-mode policy: it mounts
|
|
13
|
+
* `<NpColorSchemeScript />` so the saved `np-color-scheme`
|
|
14
|
+
* cookie / `prefers-color-scheme` choice is applied to
|
|
15
|
+
* `<html data-theme="…">` before first paint, and the dark
|
|
16
|
+
* variants ride on the rules in `defaultThemeCss`.
|
|
17
|
+
*
|
|
18
|
+
* Dark mode is no longer auto-wired by the framework — every
|
|
19
|
+
* theme decides whether to ship light/dark switching, what
|
|
20
|
+
* tokens it flips, and how the toggle UX looks. Themes that
|
|
21
|
+
* want a different policy (no dark mode, time-of-day,
|
|
22
|
+
* seasonal palette, …) simply omit this script and the
|
|
23
|
+
* dark CSS overrides.
|
|
24
|
+
*/
|
|
25
|
+
declare function DefaultShell({ children }: NpThemeShellProps): ReactNode;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default theme header — server component. Reads the
|
|
29
|
+
* `header` navigation menu and renders the desktop / mobile
|
|
30
|
+
* surfaces in one go:
|
|
31
|
+
*
|
|
32
|
+
* - Desktop (≥768px): inline link list, search bar, lang
|
|
33
|
+
* picker, dark toggle, member widget.
|
|
34
|
+
* - Mobile (<768px): the inline list collapses (CSS-only) and
|
|
35
|
+
* a hamburger button opens a slide-in drawer (`<MobileNav />`,
|
|
36
|
+
* a small client component that owns its own open/closed
|
|
37
|
+
* state). The same nav items feed both surfaces — markup is
|
|
38
|
+
* server-rendered once and reused.
|
|
39
|
+
*
|
|
40
|
+
* The header is `position: sticky` (see styles.ts) so the search
|
|
41
|
+
* + member widget stay reachable as the page scrolls.
|
|
42
|
+
*/
|
|
43
|
+
declare function DefaultHeader(): Promise<react_jsx_runtime.JSX.Element>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Social-link strip read from `process.env.NP_SOCIAL_*` so
|
|
47
|
+
* sites can light up the footer icons without forking the theme.
|
|
48
|
+
* Empty when no env vars are set — the column collapses cleanly.
|
|
49
|
+
*
|
|
50
|
+
* Recognized env vars:
|
|
51
|
+
* NP_SOCIAL_GITHUB → https://github.com/<handle>
|
|
52
|
+
* NP_SOCIAL_TWITTER → https://twitter.com/<handle> or x.com URL
|
|
53
|
+
* NP_SOCIAL_LINKEDIN → company / personal URL
|
|
54
|
+
* NP_SOCIAL_MASTODON → https://mastodon.social/@<handle>
|
|
55
|
+
* NP_SOCIAL_RSS → defaults to /feed.xml; set explicit URL to override
|
|
56
|
+
* NP_SOCIAL_EMAIL → mailto: address
|
|
57
|
+
*/
|
|
58
|
+
declare function SocialLinks(): react_jsx_runtime.JSX.Element | null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Card representation of a post in a list / grid context. Used
|
|
62
|
+
* by the post-list template's grid + the related-posts strip on
|
|
63
|
+
* post detail.
|
|
64
|
+
*
|
|
65
|
+
* Renders a small card with title, optional cover image, excerpt,
|
|
66
|
+
* date, and reading time when those fields exist on the doc.
|
|
67
|
+
* Stays defensive on field shapes — sites can fork the posts
|
|
68
|
+
* collection schema, so we use type guards rather than a hard
|
|
69
|
+
* shape requirement.
|
|
70
|
+
*/
|
|
71
|
+
interface PostCardDoc {
|
|
72
|
+
id?: string;
|
|
73
|
+
slug?: string;
|
|
74
|
+
title?: string;
|
|
75
|
+
excerpt?: string;
|
|
76
|
+
cover?: {
|
|
77
|
+
url?: string;
|
|
78
|
+
alt?: string;
|
|
79
|
+
} | string | null;
|
|
80
|
+
publishedAt?: string | Date;
|
|
81
|
+
readingTime?: number | string;
|
|
82
|
+
author?: {
|
|
83
|
+
name?: string;
|
|
84
|
+
} | string;
|
|
85
|
+
}
|
|
86
|
+
interface PostCardProps {
|
|
87
|
+
doc: PostCardDoc;
|
|
88
|
+
/** Visual variant. "grid" is the default; "feature" is bigger and lets the cover image stretch. */
|
|
89
|
+
variant?: "grid" | "feature";
|
|
90
|
+
}
|
|
91
|
+
declare function PostCard({ doc, variant }: PostCardProps): react_jsx_runtime.JSX.Element;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Server-rendered pagination strip. Templates pass in the current
|
|
95
|
+
* page + total count + URL builder; we render Prev / page numbers
|
|
96
|
+
* / Next via next/link so navigation stays SPA-style and a
|
|
97
|
+
* no-JS user still gets working anchors.
|
|
98
|
+
*/
|
|
99
|
+
interface PaginationProps {
|
|
100
|
+
page: number;
|
|
101
|
+
totalPages: number;
|
|
102
|
+
/** Builds the href for a given page number. Letting the
|
|
103
|
+
* caller own this means /blog?page= and /search?q=foo&page=
|
|
104
|
+
* share the same component. */
|
|
105
|
+
hrefForPage: (page: number) => string;
|
|
106
|
+
/** How many neighbours to show on each side of the active
|
|
107
|
+
* page before collapsing to "…". Default 1, like GitHub. */
|
|
108
|
+
siblings?: number;
|
|
109
|
+
}
|
|
110
|
+
declare function Pagination({ page, totalPages, hrefForPage, siblings }: PaginationProps): react_jsx_runtime.JSX.Element | null;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Landing-page template — full-bleed hero from the doc's first
|
|
114
|
+
* block, then the rest of the blocks render edge-to-edge so each
|
|
115
|
+
* one (Hero / FeatureGrid / CTA / Pricing) can use the full
|
|
116
|
+
* viewport width. The page's `title` and `seoDescription` form a
|
|
117
|
+
* fallback hero when the doc has no blocks yet.
|
|
118
|
+
*
|
|
119
|
+
* For pages where the operator wants a single max-width column
|
|
120
|
+
* and a sticky table of contents, pick the "default" template
|
|
121
|
+
* instead.
|
|
122
|
+
*/
|
|
123
|
+
declare function PageLandingTemplate({ doc, blockCtx }: NpTemplateRenderProps): react_jsx_runtime.JSX.Element;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Page template with a sticky sidebar on the right. Suited to
|
|
127
|
+
* documentation / knowledge-base pages — the main column carries
|
|
128
|
+
* the body, the aside carries supporting links / version pickers
|
|
129
|
+
* / contributor cards.
|
|
130
|
+
*
|
|
131
|
+
* The aside is sourced from `doc.sidebar` (free-form blocks) when
|
|
132
|
+
* present; sites that don't model a sidebar field on their pages
|
|
133
|
+
* collection just see the main column. We keep the aside slot
|
|
134
|
+
* always-rendered (with a fallback "On this page" placeholder) so
|
|
135
|
+
* the layout doesn't reflow when an editor toggles the field.
|
|
136
|
+
*/
|
|
137
|
+
declare function PageSidebarTemplate({ doc, blockCtx }: NpTemplateRenderProps): react_jsx_runtime.JSX.Element;
|
|
138
|
+
|
|
139
|
+
declare function PostDefaultTemplate({ doc }: NpTemplateRenderProps): react_jsx_runtime.JSX.Element;
|
|
140
|
+
|
|
141
|
+
declare function PostListTemplate({ doc }: NpTemplateRenderProps): react_jsx_runtime.JSX.Element;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Layout-level CSS for `@nexpress/theme-default`. Production polish
|
|
145
|
+
* lives here: sticky header with mobile drawer, four-column footer
|
|
146
|
+
* with social + newsletter, post detail / list, page templates
|
|
147
|
+
* (default / wide / landing / sidebar), pagination, and a typography
|
|
148
|
+
* ramp that holds together across page widths.
|
|
149
|
+
*
|
|
150
|
+
* Design tokens come from `--np-color-*` / `--np-radius-*` /
|
|
151
|
+
* `--np-font-*` (set in apps/web/src/app/globals.css and switched
|
|
152
|
+
* by `[data-theme="dark"]` further down). Themes that want a
|
|
153
|
+
* different palette override the same custom properties — this
|
|
154
|
+
* file is structural.
|
|
155
|
+
*
|
|
156
|
+
* The framework injects this string as a `<style data-np-theme="default">`
|
|
157
|
+
* tag at SSR time so the rules race the document with no extra
|
|
158
|
+
* stylesheet round-trip.
|
|
159
|
+
*/
|
|
160
|
+
declare const defaultThemeCss: string;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* `@nexpress/theme-default` — v0.1-era baseline theme.
|
|
164
|
+
*
|
|
165
|
+
* **Status (v0.2):** kept for back-compat. The v0.2 reference
|
|
166
|
+
* themes are `theme-magazine` / `theme-docs` / `theme-portfolio`
|
|
167
|
+
* — they exercise the new contract surfaces (manifest.requires,
|
|
168
|
+
* settingsSchema, blocks, patterns, navLocations, archives,
|
|
169
|
+
* routes, seo). New sites should start from one of those.
|
|
170
|
+
*
|
|
171
|
+
* `theme-default` doesn't declare v0.2 surfaces: operators using
|
|
172
|
+
* it skip the no-code-customization workflow (no admin auto-
|
|
173
|
+
* form for theme settings, no `theme:install` data-shape
|
|
174
|
+
* checks, no theme-shipped blocks/patterns). It remains a valid
|
|
175
|
+
* `defineTheme` caller — production sites pinned to v0.1 keep
|
|
176
|
+
* working — but consider migrating to a v0.2 reference if you
|
|
177
|
+
* want operator-tunable settings.
|
|
178
|
+
*
|
|
179
|
+
* Production-grade defaults: sticky header with a mobile drawer,
|
|
180
|
+
* a four-column footer (brand / sitemap / resources / newsletter)
|
|
181
|
+
* with optional social icons, post list / detail templates that
|
|
182
|
+
* surface excerpt / cover / tags / reading time, and three page
|
|
183
|
+
* templates (default centered column, edge-to-edge wide, marketing
|
|
184
|
+
* landing, doc-style sidebar). All CSS is theme-owned so the
|
|
185
|
+
* framework drops it as a single `<style data-np-theme="default">`
|
|
186
|
+
* tag at SSR time — no extra round-trip.
|
|
187
|
+
*
|
|
188
|
+
* Sites brand by overriding the design tokens (`--np-color-*` etc).
|
|
189
|
+
*/
|
|
190
|
+
declare const defaultTheme: _nexpress_theme.NpTheme;
|
|
191
|
+
|
|
192
|
+
export { DefaultHeader, DefaultShell, PageLandingTemplate, PageSidebarTemplate, Pagination, type PaginationProps, PostCard, type PostCardDoc, type PostCardProps, PostDefaultTemplate, PostListTemplate, SocialLinks, defaultTheme, defaultThemeCss };
|