@cedros/data-react 0.1.5 → 0.1.6

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/README.md CHANGED
@@ -31,9 +31,14 @@ Packaging smoke:
31
31
  ## Package exports
32
32
 
33
33
  - `@cedros/data-react`
34
- - `defaultPageTemplates`
35
- - `DefaultPageKey`, `PageTemplateContract`
36
- - re-exports admin + site-template modules
34
+ - `CedrosDataProvider`, `useCedrosDataTheme`, `useCedrosDataThemeOptional`
35
+ - `CmsContent`, `getOrCreateVisitorId`, `sanitizeCmsHtml`, `renderCmsMarkdown`
36
+ - theme/page/content types
37
+ - `@cedros/data-react/server`
38
+ - `generatePageMetadata`, `buildPageMetadata`
39
+ - `fetchBlogPost`, `listContentSlugs`, `listBlogSlugs`, `listLearnPathIds`
40
+ - `loadSitemapEntries`
41
+ - all accept `ServerFetchOptions` with optional `apiKey` (falls back to `CEDROS_X_API_KEY` env)
37
42
  - `@cedros/data-react/admin`
38
43
  - `cedrosDataPlugin`
39
44
  - section IDs/groups/types
@@ -91,6 +96,9 @@ Content:
91
96
  - `pages`
92
97
  - `navigation`
93
98
  - `site-settings`
99
+ - `layout` — nav width, link position/style, footer width
100
+ - `tipping`
101
+ - `monetization`
94
102
 
95
103
  Data model:
96
104
  - `collections`
@@ -102,8 +110,44 @@ Operations:
102
110
  - `data-ops`
103
111
  - `history`
104
112
 
113
+ ## Theme system (CedrosDataProvider)
114
+
115
+ `CedrosDataProvider` matches the theme API used by cedros-login and cedros-pay:
116
+
117
+ ```tsx
118
+ import { CedrosDataProvider } from "@cedros/data-react";
119
+
120
+ <CedrosDataProvider
121
+ theme="dark"
122
+ themeOverrides={{
123
+ "--cedros-background": "#292524",
124
+ "--cedros-foreground": "#f5f5f4",
125
+ "--cedros-border": "#44403c",
126
+ }}
127
+ >
128
+ <SiteLayout ... />
129
+ </CedrosDataProvider>
130
+ ```
131
+
132
+ Props:
133
+ - `theme` — `'light'` | `'dark'` (default: `'light'`)
134
+ - `themeOverrides` — typed `CedrosDataThemeOverrides` with 13 tokens + open-ended `[key: string]`
135
+ - `unstyled` — disables all default className/style (for custom design systems)
136
+
137
+ Hook:
138
+ ```tsx
139
+ import { useCedrosDataTheme } from "@cedros/data-react";
140
+
141
+ const { mode, isDark, className, style, unstyled } = useCedrosDataTheme();
142
+ ```
143
+
144
+ Also exports `useCedrosDataThemeOptional()` which returns `null` outside a provider.
145
+
146
+ The provider applies className (`.cedros-dark`) and inline style overrides on the **same element** so overrides always win via CSS specificity — matching cedros-pay/login behavior.
147
+
105
148
  ## Styling and dark mode
106
149
 
150
+ - All component styles use `var(--cedros-*, fallback)` — no hardcoded colors, even in dark mode.
107
151
  - Admin UIs use tokenized variables aligned with cedros-login/cedros-pay conventions.
108
152
  - Dark mode responds to both:
109
153
  - `.cedros-dark`
@@ -117,11 +161,38 @@ import "@cedros/data-react/admin/styles.css";
117
161
  ## Site templates and components
118
162
 
119
163
  Layouts/navigation:
120
- - `SiteLayout`
121
- - `TopNav`
122
- - `SiteFooter`
164
+ - `SiteLayout` — accepts `layout?: SiteLayoutOptions` for nav/footer configuration
165
+ - `TopNav` — accepts `layout?: NavLayoutOptions`
166
+ - `SiteFooter` — accepts `layout?: FooterLayoutOptions`
123
167
  - `DashboardShell`
124
168
 
169
+ ### Layout options
170
+
171
+ Layout preferences are managed via the admin "Layout" section (stored as `site_settings/layout`). Pass them to `SiteLayout`:
172
+
173
+ ```tsx
174
+ <SiteLayout
175
+ siteTitle="My Site"
176
+ navigation={navItems}
177
+ layout={{
178
+ nav: {
179
+ width: "full", // "contained" (default) | "full"
180
+ linkPosition: "center", // "right" (default) | "center"
181
+ linkStyle: "text", // "pill" (default) | "text"
182
+ },
183
+ footer: {
184
+ width: "full", // "contained" (default) | "full"
185
+ },
186
+ }}
187
+ >
188
+ {children}
189
+ </SiteLayout>
190
+ ```
191
+
192
+ - **width: "full"** — logo and actions stretch to viewport edges
193
+ - **linkPosition: "center"** — nav links centered between brand and right slot
194
+ - **linkStyle: "text"** — plain text links instead of pill buttons
195
+
125
196
  Core page templates:
126
197
  - `HomePageTemplate`
127
198
  - `ContactPageTemplate`
@@ -248,6 +319,30 @@ Docs/blog templates default to `bodyMarkdown`.
248
319
 
249
320
  This keeps markdown as the safe default and avoids unsafe HTML rendering by default.
250
321
 
322
+ ## Server helpers and API key
323
+
324
+ `@cedros/data-react/server` provides Next.js data-fetching helpers that call cedros-data directly:
325
+
326
+ ```tsx
327
+ import { generatePageMetadata, loadSitemapEntries, fetchBlogPost } from "@cedros/data-react/server";
328
+
329
+ // All helpers accept ServerFetchOptions:
330
+ const meta = await generatePageMetadata("about", {
331
+ serverUrl: process.env.CEDROS_DATA_URL,
332
+ apiKey: process.env.CEDROS_X_API_KEY, // optional, sent as x-api-key header
333
+ });
334
+ ```
335
+
336
+ The API key resolves from (in order):
337
+ 1. `options.apiKey` if explicitly passed
338
+ 2. `process.env.CEDROS_X_API_KEY` environment variable
339
+ 3. No header sent (unauthenticated — works with direct cedros-data access)
340
+
341
+ Environment variables for server URL resolution:
342
+ 1. `options.serverUrl` if explicitly passed
343
+ 2. `CEDROS_DATA_URL`
344
+ 3. `NEXT_PUBLIC_BACKEND_API_URL`
345
+
251
346
  ## Default page contract
252
347
 
253
348
  The package exports the default page template contract used by server bootstrap:
@@ -12,4 +12,5 @@ export declare const Icons: {
12
12
  history: ReactNode;
13
13
  tipping: ReactNode;
14
14
  monetization: ReactNode;
15
+ layout: ReactNode;
15
16
  };
@@ -14,5 +14,6 @@ export const Icons = {
14
14
  dataOps: icon("M4 4h16v16H4z M8 8h8 M8 12h8 M8 16h4"),
15
15
  history: icon("M12 8v5l3 2M20 12a8 8 0 11-2.34-5.66"),
16
16
  tipping: icon("M12 2a7 7 0 00-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 00-7-7z"),
17
- monetization: icon("M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6")
17
+ monetization: icon("M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"),
18
+ layout: icon("M4 3h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4a1 1 0 011-1zM3 9h18M9 21V9")
18
19
  };
@@ -14,6 +14,7 @@ const MediaSection = lazy(() => import("./sections/media/MediaSection.js"));
14
14
  const HistorySection = lazy(() => import("./sections/HistorySection.js"));
15
15
  const TippingSection = lazy(() => import("./sections/TippingSection.js"));
16
16
  const MonetizationSection = lazy(() => import("./sections/MonetizationSection.js"));
17
+ const LayoutSection = lazy(() => import("./sections/LayoutSection.js"));
17
18
  export const cedrosDataPlugin = {
18
19
  id: "cedros-data",
19
20
  name: "Cedros Data",
@@ -114,6 +115,14 @@ export const cedrosDataPlugin = {
114
115
  group: CEDROS_DATA_GROUPS.content,
115
116
  order: 5,
116
117
  requiredPermission: "data:settings:read"
118
+ },
119
+ {
120
+ id: CEDROS_DATA_SECTIONS.layout,
121
+ label: "Layout",
122
+ icon: Icons.layout,
123
+ group: CEDROS_DATA_GROUPS.content,
124
+ order: 6,
125
+ requiredPermission: "data:settings:read"
117
126
  }
118
127
  ],
119
128
  groups: [
@@ -133,7 +142,8 @@ export const cedrosDataPlugin = {
133
142
  [CEDROS_DATA_SECTIONS.dataOps]: DataOpsSection,
134
143
  [CEDROS_DATA_SECTIONS.history]: HistorySection,
135
144
  [CEDROS_DATA_SECTIONS.tipping]: TippingSection,
136
- [CEDROS_DATA_SECTIONS.monetization]: MonetizationSection
145
+ [CEDROS_DATA_SECTIONS.monetization]: MonetizationSection,
146
+ [CEDROS_DATA_SECTIONS.layout]: LayoutSection
137
147
  },
138
148
  createPluginContext(hostContext) {
139
149
  const custom = readCedrosDataCustomContext(hostContext);
@@ -11,8 +11,9 @@ export declare const CEDROS_DATA_SECTIONS: {
11
11
  readonly history: "history";
12
12
  readonly tipping: "tipping";
13
13
  readonly monetization: "monetization";
14
+ readonly layout: "layout";
14
15
  };
15
- export declare const CEDROS_DATA_SECTION_IDS: readonly ["cedros-data:pages", "cedros-data:navigation", "cedros-data:site-settings", "cedros-data:media", "cedros-data:collections", "cedros-data:schema-designer", "cedros-data:contract-verify", "cedros-data:custom-data", "cedros-data:data-ops", "cedros-data:history", "cedros-data:tipping", "cedros-data:monetization"];
16
+ export declare const CEDROS_DATA_SECTION_IDS: readonly ["cedros-data:pages", "cedros-data:navigation", "cedros-data:site-settings", "cedros-data:media", "cedros-data:collections", "cedros-data:schema-designer", "cedros-data:contract-verify", "cedros-data:custom-data", "cedros-data:data-ops", "cedros-data:history", "cedros-data:tipping", "cedros-data:monetization", "cedros-data:layout"];
16
17
  export declare const CEDROS_DATA_GROUPS: {
17
18
  readonly content: "Content";
18
19
  readonly dataModel: "Data Model";
@@ -10,7 +10,8 @@ export const CEDROS_DATA_SECTIONS = {
10
10
  dataOps: "data-ops",
11
11
  history: "history",
12
12
  tipping: "tipping",
13
- monetization: "monetization"
13
+ monetization: "monetization",
14
+ layout: "layout"
14
15
  };
15
16
  export const CEDROS_DATA_SECTION_IDS = [
16
17
  "cedros-data:pages",
@@ -24,7 +25,8 @@ export const CEDROS_DATA_SECTION_IDS = [
24
25
  "cedros-data:data-ops",
25
26
  "cedros-data:history",
26
27
  "cedros-data:tipping",
27
- "cedros-data:monetization"
28
+ "cedros-data:monetization",
29
+ "cedros-data:layout"
28
30
  ];
29
31
  export const CEDROS_DATA_GROUPS = {
30
32
  content: "Content",
@@ -0,0 +1,2 @@
1
+ import type { AdminSectionProps } from "../types.js";
2
+ export default function LayoutSection({ pluginContext }: AdminSectionProps): React.JSX.Element;
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { requestJson } from "../api.js";
4
+ import { AdminButton, Card, SelectInput, StatusNotice } from "../components.js";
5
+ const DEFAULT_LAYOUT = {
6
+ navWidth: "contained",
7
+ navLinkPosition: "right",
8
+ navLinkStyle: "pill",
9
+ footerWidth: "contained"
10
+ };
11
+ export default function LayoutSection({ pluginContext }) {
12
+ const canWrite = pluginContext.hasPermission("data:settings:write");
13
+ const [settings, setSettings] = useState(DEFAULT_LAYOUT);
14
+ const [status, setStatus] = useState("");
15
+ const [tone, setTone] = useState("neutral");
16
+ const [loading, setLoading] = useState(false);
17
+ const load = useCallback(async () => {
18
+ setLoading(true);
19
+ setStatus("");
20
+ try {
21
+ const entries = await requestJson(pluginContext, "/entries/query", {
22
+ method: "POST",
23
+ body: {
24
+ collection_name: "site_settings",
25
+ entry_keys: ["layout"],
26
+ contains: null,
27
+ limit: 1,
28
+ offset: 0
29
+ }
30
+ });
31
+ if (entries.length > 0) {
32
+ setSettings(parseLayoutSettings(entries[0].payload));
33
+ }
34
+ }
35
+ catch (error) {
36
+ setStatus(error.message);
37
+ setTone("error");
38
+ }
39
+ finally {
40
+ setLoading(false);
41
+ }
42
+ }, [pluginContext]);
43
+ useEffect(() => {
44
+ void load();
45
+ }, [load]);
46
+ const save = useCallback(async () => {
47
+ if (!canWrite) {
48
+ setStatus("Missing data:settings:write permission.");
49
+ setTone("error");
50
+ return;
51
+ }
52
+ setLoading(true);
53
+ setStatus("");
54
+ try {
55
+ await requestJson(pluginContext, "/entries/upsert", {
56
+ method: "POST",
57
+ body: {
58
+ collection_name: "site_settings",
59
+ entry_key: "layout",
60
+ payload: settings
61
+ }
62
+ });
63
+ setStatus("Layout settings saved.");
64
+ setTone("success");
65
+ }
66
+ catch (error) {
67
+ setStatus(error.message);
68
+ setTone("error");
69
+ }
70
+ finally {
71
+ setLoading(false);
72
+ }
73
+ }, [canWrite, pluginContext, settings]);
74
+ const update = useCallback((key, value) => {
75
+ setSettings((prev) => ({ ...prev, [key]: value }));
76
+ }, []);
77
+ return (_jsxs("div", { className: "cedros-data", children: [_jsxs("header", { className: "cedros-data__header", children: [_jsxs("div", { children: [_jsx("h2", { className: "cedros-data__title", children: "Layout" }), _jsx("p", { className: "cedros-data__subtitle", children: "Configure the site navigation bar and footer appearance." }), !canWrite && (_jsx("p", { className: "cedros-data__subtitle", children: "Read-only. Missing data:settings:write permission." }))] }), _jsxs("div", { className: "cedros-data-actions", children: [_jsx(AdminButton, { variant: "secondary", onClick: () => void load(), disabled: loading, children: "Refresh" }), _jsx(AdminButton, { variant: "primary", onClick: () => void save(), disabled: loading || !canWrite, children: "Save" })] })] }), _jsxs("div", { className: "cedros-data-grid cedros-data-grid--two", children: [_jsxs(Card, { title: "Navigation Bar", subtitle: "Controls the top nav header layout.", children: [_jsxs(SelectInput, { label: "Width", value: settings.navWidth, onChange: (e) => update("navWidth", e.target.value), disabled: !canWrite, children: [_jsx("option", { value: "contained", children: "Contained (max-width)" }), _jsx("option", { value: "full", children: "Full width (edge to edge)" })] }), _jsxs(SelectInput, { label: "Link Position", value: settings.navLinkPosition, onChange: (e) => update("navLinkPosition", e.target.value), disabled: !canWrite, children: [_jsx("option", { value: "right", children: "Right (next to actions)" }), _jsx("option", { value: "center", children: "Center" })] }), _jsxs(SelectInput, { label: "Link Style", value: settings.navLinkStyle, onChange: (e) => update("navLinkStyle", e.target.value), disabled: !canWrite, children: [_jsx("option", { value: "pill", children: "Pill buttons" }), _jsx("option", { value: "text", children: "Plain text links" })] })] }), _jsx(Card, { title: "Footer", subtitle: "Controls the site footer layout.", children: _jsxs(SelectInput, { label: "Width", value: settings.footerWidth, onChange: (e) => update("footerWidth", e.target.value), disabled: !canWrite, children: [_jsx("option", { value: "contained", children: "Contained (max-width)" }), _jsx("option", { value: "full", children: "Full width (edge to edge)" })] }) })] }), status && _jsx(StatusNotice, { tone: tone, message: status })] }));
78
+ }
79
+ function parseLayoutSettings(payload) {
80
+ return {
81
+ navWidth: isNavWidth(payload.navWidth) ? payload.navWidth : DEFAULT_LAYOUT.navWidth,
82
+ navLinkPosition: isNavLinkPosition(payload.navLinkPosition)
83
+ ? payload.navLinkPosition
84
+ : DEFAULT_LAYOUT.navLinkPosition,
85
+ navLinkStyle: isNavLinkStyle(payload.navLinkStyle)
86
+ ? payload.navLinkStyle
87
+ : DEFAULT_LAYOUT.navLinkStyle,
88
+ footerWidth: isFooterWidth(payload.footerWidth)
89
+ ? payload.footerWidth
90
+ : DEFAULT_LAYOUT.footerWidth
91
+ };
92
+ }
93
+ function isNavWidth(v) {
94
+ return v === "contained" || v === "full";
95
+ }
96
+ function isNavLinkPosition(v) {
97
+ return v === "center" || v === "right";
98
+ }
99
+ function isNavLinkStyle(v) {
100
+ return v === "text" || v === "pill";
101
+ }
102
+ function isFooterWidth(v) {
103
+ return v === "contained" || v === "full";
104
+ }
@@ -22,17 +22,17 @@
22
22
 
23
23
  .cedros-dark .cedros-data,
24
24
  .cedros-admin.cedros-admin--dark .cedros-data {
25
- --cd-bg: hsl(222.2, 84%, 4.9%);
26
- --cd-fg: hsl(210, 40%, 98%);
27
- --cd-muted: hsl(215, 20.2%, 65.1%);
28
- --cd-muted-bg: hsl(217.2, 32.6%, 17.5%);
29
- --cd-border: hsl(217.2, 32.6%, 17.5%);
30
- --cd-accent: hsl(217.2, 32.6%, 17.5%);
31
- --cd-ring: hsl(212.7, 26.8%, 83.9%);
32
- --cd-primary: hsl(210, 40%, 98%);
33
- --cd-primary-fg: hsl(222.2, 47.4%, 11.2%);
34
- --cd-success: hsl(142, 71%, 45%);
35
- --cd-error: hsl(0, 84.2%, 60.2%);
25
+ --cd-bg: var(--cedros-background, hsl(222.2, 84%, 4.9%));
26
+ --cd-fg: var(--cedros-foreground, hsl(210, 40%, 98%));
27
+ --cd-muted: var(--cedros-muted-foreground, hsl(215, 20.2%, 65.1%));
28
+ --cd-muted-bg: var(--cedros-muted, hsl(217.2, 32.6%, 17.5%));
29
+ --cd-border: var(--cedros-border, hsl(217.2, 32.6%, 17.5%));
30
+ --cd-accent: var(--cedros-accent, hsl(217.2, 32.6%, 17.5%));
31
+ --cd-ring: var(--cedros-ring, hsl(212.7, 26.8%, 83.9%));
32
+ --cd-primary: var(--cedros-primary, hsl(210, 40%, 98%));
33
+ --cd-primary-fg: var(--cedros-primary-foreground, hsl(222.2, 47.4%, 11.2%));
34
+ --cd-success: var(--cedros-success, hsl(142, 71%, 45%));
35
+ --cd-error: var(--cedros-error, hsl(0, 84.2%, 60.2%));
36
36
  }
37
37
 
38
38
  .cedros-data__header {
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CedrosDataThemeOverrides, CedrosDataThemeValue, ThemeMode } from "./theme.js";
3
+ export interface CedrosDataProviderProps {
4
+ /** Light or dark mode. */
5
+ theme?: ThemeMode;
6
+ /** CSS variable overrides applied as inline styles. */
7
+ themeOverrides?: CedrosDataThemeOverrides;
8
+ /** When true, no default className or style is applied. */
9
+ unstyled?: boolean;
10
+ children: ReactNode;
11
+ }
12
+ /**
13
+ * Theme provider for cedros-data components.
14
+ *
15
+ * Applies `className` and inline `style` on the **same element** so that
16
+ * overrides always win over class-selector-based CSS variable declarations.
17
+ */
18
+ export declare function CedrosDataProvider({ theme, themeOverrides, unstyled, children, }: CedrosDataProviderProps): React.JSX.Element;
19
+ /**
20
+ * Returns the current theme context.
21
+ *
22
+ * Must be called inside a `<CedrosDataProvider>`.
23
+ */
24
+ export declare function useCedrosDataTheme(): CedrosDataThemeValue;
25
+ /**
26
+ * Returns the current theme context, or `null` when outside a provider.
27
+ */
28
+ export declare function useCedrosDataThemeOptional(): CedrosDataThemeValue | null;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
4
+ const CedrosDataThemeContext = createContext(null);
5
+ /**
6
+ * Theme provider for cedros-data components.
7
+ *
8
+ * Applies `className` and inline `style` on the **same element** so that
9
+ * overrides always win over class-selector-based CSS variable declarations.
10
+ */
11
+ export function CedrosDataProvider({ theme = "light", themeOverrides, unstyled = false, children, }) {
12
+ const stableOverrides = useStableOverrides(themeOverrides);
13
+ const value = useMemo(() => {
14
+ const isDark = theme === "dark";
15
+ if (unstyled) {
16
+ return { mode: theme, isDark, className: "", style: {}, unstyled: true, overrides: stableOverrides };
17
+ }
18
+ const className = isDark ? "cedros-dark" : "";
19
+ const style = overridesToStyle(stableOverrides);
20
+ return { mode: theme, isDark, className, style, unstyled: false, overrides: stableOverrides };
21
+ }, [theme, unstyled, stableOverrides]);
22
+ return (_jsx(CedrosDataThemeContext.Provider, { value: value, children: _jsx("div", { className: value.className || undefined, style: value.style, children: children }) }));
23
+ }
24
+ /**
25
+ * Returns the current theme context.
26
+ *
27
+ * Must be called inside a `<CedrosDataProvider>`.
28
+ */
29
+ export function useCedrosDataTheme() {
30
+ const ctx = useContext(CedrosDataThemeContext);
31
+ if (!ctx) {
32
+ throw new Error("useCedrosDataTheme must be used within CedrosDataProvider");
33
+ }
34
+ return ctx;
35
+ }
36
+ /**
37
+ * Returns the current theme context, or `null` when outside a provider.
38
+ */
39
+ export function useCedrosDataThemeOptional() {
40
+ return useContext(CedrosDataThemeContext);
41
+ }
42
+ // -- internals ----------------------------------------------------------------
43
+ /** Deep-compare overrides so parent inline objects don't cause re-renders. */
44
+ function useStableOverrides(overrides) {
45
+ const ref = useRef(overrides);
46
+ const [stable, setStable] = useState(overrides);
47
+ useEffect(() => {
48
+ if (shallowEqual(ref.current, overrides))
49
+ return;
50
+ ref.current = overrides;
51
+ setStable(overrides);
52
+ }, [overrides]);
53
+ return stable;
54
+ }
55
+ function shallowEqual(a, b) {
56
+ if (a === b)
57
+ return true;
58
+ if (!a || !b)
59
+ return false;
60
+ const keysA = Object.keys(a);
61
+ const keysB = Object.keys(b);
62
+ if (keysA.length !== keysB.length)
63
+ return false;
64
+ return keysA.every((key) => a[key] === b[key]);
65
+ }
66
+ function overridesToStyle(overrides) {
67
+ if (!overrides)
68
+ return {};
69
+ const style = {};
70
+ for (const [key, value] of Object.entries(overrides)) {
71
+ if (value !== undefined) {
72
+ style[key] = value;
73
+ }
74
+ }
75
+ return style;
76
+ }
@@ -4,6 +4,8 @@
4
4
  * These exports are safe to use in React Client Components and
5
5
  * do not import any server-only modules.
6
6
  */
7
+ export { CedrosDataProvider, useCedrosDataTheme, useCedrosDataThemeOptional, type CedrosDataProviderProps, } from "./CedrosDataProvider.js";
8
+ export type { ThemeMode, CedrosDataThemeOverrides, CedrosDataThemeValue, } from "./theme.js";
7
9
  export { CmsContent, type CmsContentProps } from "./CmsContent.js";
8
10
  export { getOrCreateVisitorId } from "./visitor.js";
9
11
  export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
@@ -4,6 +4,7 @@
4
4
  * These exports are safe to use in React Client Components and
5
5
  * do not import any server-only modules.
6
6
  */
7
+ export { CedrosDataProvider, useCedrosDataTheme, useCedrosDataThemeOptional, } from "./CedrosDataProvider.js";
7
8
  export { CmsContent } from "./CmsContent.js";
8
9
  export { getOrCreateVisitorId } from "./visitor.js";
9
10
  export { sanitizeCmsHtml, renderCmsMarkdown } from "./sanitize.js";
@@ -0,0 +1,33 @@
1
+ /** Theme mode selector. */
2
+ export type ThemeMode = "light" | "dark";
3
+ /**
4
+ * Typed theme token overrides using CSS variable names.
5
+ *
6
+ * Matches the `--cedros-*` variable namespace shared across
7
+ * cedros-login, cedros-pay, and cedros-data.
8
+ */
9
+ export interface CedrosDataThemeOverrides {
10
+ "--cedros-background"?: string;
11
+ "--cedros-foreground"?: string;
12
+ "--cedros-muted"?: string;
13
+ "--cedros-muted-foreground"?: string;
14
+ "--cedros-border"?: string;
15
+ "--cedros-link"?: string;
16
+ "--cedros-primary"?: string;
17
+ "--cedros-primary-foreground"?: string;
18
+ "--cedros-accent"?: string;
19
+ "--cedros-ring"?: string;
20
+ "--cedros-radius"?: string;
21
+ "--cedros-success"?: string;
22
+ "--cedros-error"?: string;
23
+ [key: string]: string | undefined;
24
+ }
25
+ /** Value returned by `useCedrosDataTheme()`. */
26
+ export interface CedrosDataThemeValue {
27
+ mode: ThemeMode;
28
+ isDark: boolean;
29
+ className: string;
30
+ style: React.CSSProperties;
31
+ unstyled: boolean;
32
+ overrides?: CedrosDataThemeOverrides;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,9 @@
1
1
  import type { ReactNode } from "react";
2
+ /** Layout options for the site footer. */
3
+ export interface FooterLayoutOptions {
4
+ /** Full-width stretches content to viewport edges. Default: "contained". */
5
+ width?: "contained" | "full";
6
+ }
2
7
  export interface SiteFooterLink {
3
8
  key: string;
4
9
  label: string;
@@ -9,5 +14,6 @@ export interface SiteFooterProps {
9
14
  note?: string;
10
15
  links?: SiteFooterLink[];
11
16
  rightSlot?: ReactNode;
17
+ layout?: FooterLayoutOptions;
12
18
  }
13
- export declare function SiteFooter({ siteTitle, note, links, rightSlot }: SiteFooterProps): React.JSX.Element;
19
+ export declare function SiteFooter({ siteTitle, note, links, rightSlot, layout }: SiteFooterProps): React.JSX.Element;
@@ -1,4 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export function SiteFooter({ siteTitle, note, links = [], rightSlot }) {
3
- return (_jsx("footer", { className: "cedros-site__footer", children: _jsxs("div", { className: "cedros-site__container cedros-site__footer-inner", children: [_jsxs("div", { className: "cedros-site__footer-brand", children: [_jsx("span", { children: siteTitle }), note && _jsx("span", { children: note })] }), links.length > 0 && (_jsx("nav", { className: "cedros-site__footer-links", "aria-label": "Footer", children: links.map((link) => (_jsx("a", { href: link.route, className: "cedros-site__footer-link", children: link.label }, link.key))) })), rightSlot && _jsx("div", { className: "cedros-site__footer-right", children: rightSlot })] }) }));
2
+ export function SiteFooter({ siteTitle, note, links = [], rightSlot, layout }) {
3
+ const footerClasses = layout?.width === "full"
4
+ ? "cedros-site__footer cedros-site__footer--full"
5
+ : "cedros-site__footer";
6
+ return (_jsx("footer", { className: footerClasses, children: _jsxs("div", { className: "cedros-site__container cedros-site__footer-inner", children: [_jsxs("div", { className: "cedros-site__footer-brand", children: [_jsx("span", { children: siteTitle }), note && _jsx("span", { children: note })] }), links.length > 0 && (_jsx("nav", { className: "cedros-site__footer-links", "aria-label": "Footer", children: links.map((link) => (_jsx("a", { href: link.route, className: "cedros-site__footer-link", children: link.label }, link.key))) })), rightSlot && _jsx("div", { className: "cedros-site__footer-right", children: rightSlot })] }) }));
4
7
  }
@@ -1,14 +1,22 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { NavLayoutOptions } from "./TopNav.js";
3
+ import type { FooterLayoutOptions } from "./SiteFooter.js";
2
4
  export interface SiteNavigationItem {
3
5
  key: string;
4
6
  label: string;
5
7
  route: string;
6
8
  }
9
+ /** Combined layout options for the full site shell. */
10
+ export interface SiteLayoutOptions {
11
+ nav?: NavLayoutOptions;
12
+ footer?: FooterLayoutOptions;
13
+ }
7
14
  export interface SiteLayoutProps {
8
15
  siteTitle: string;
9
16
  navigation: SiteNavigationItem[];
10
17
  children: ReactNode;
11
18
  brandHref?: string;
12
19
  footerNote?: string;
20
+ layout?: SiteLayoutOptions;
13
21
  }
14
- export declare function SiteLayout({ siteTitle, navigation, children, brandHref, footerNote }: SiteLayoutProps): React.JSX.Element;
22
+ export declare function SiteLayout({ siteTitle, navigation, children, brandHref, footerNote, layout }: SiteLayoutProps): React.JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SiteFooter } from "./SiteFooter.js";
3
3
  import { TopNav } from "./TopNav.js";
4
- export function SiteLayout({ siteTitle, navigation, children, brandHref = "/", footerNote = "Powered by cedros-data" }) {
5
- return (_jsxs("div", { className: "cedros-site", children: [_jsx(TopNav, { siteTitle: siteTitle, navigation: navigation, brandHref: brandHref }), _jsx("main", { className: "cedros-site__main", children: _jsx("div", { className: "cedros-site__container", children: children }) }), _jsx(SiteFooter, { siteTitle: siteTitle, note: footerNote })] }));
4
+ export function SiteLayout({ siteTitle, navigation, children, brandHref = "/", footerNote = "Powered by cedros-data", layout }) {
5
+ return (_jsxs("div", { className: "cedros-site", children: [_jsx(TopNav, { siteTitle: siteTitle, navigation: navigation, brandHref: brandHref, layout: layout?.nav }), _jsx("main", { className: "cedros-site__main", children: _jsx("div", { className: "cedros-site__container", children: children }) }), _jsx(SiteFooter, { siteTitle: siteTitle, note: footerNote, layout: layout?.footer })] }));
6
6
  }
@@ -1,10 +1,20 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { SiteNavigationItem } from "./SiteLayout.js";
3
+ /** Layout options for the top navigation bar. */
4
+ export interface NavLayoutOptions {
5
+ /** Full-width stretches logo/actions to viewport edges. Default: "contained". */
6
+ width?: "contained" | "full";
7
+ /** Where the main nav links sit. Default: "right". */
8
+ linkPosition?: "center" | "right";
9
+ /** Visual style for nav links. Default: "pill". */
10
+ linkStyle?: "text" | "pill";
11
+ }
3
12
  export interface TopNavProps {
4
13
  siteTitle: string;
5
14
  navigation: SiteNavigationItem[];
6
15
  brandHref?: string;
7
16
  rightSlot?: ReactNode;
8
17
  currentPath?: string;
18
+ layout?: NavLayoutOptions;
9
19
  }
10
- export declare function TopNav({ siteTitle, navigation, brandHref, rightSlot, currentPath }: TopNavProps): React.JSX.Element;
20
+ export declare function TopNav({ siteTitle, navigation, brandHref, rightSlot, currentPath, layout }: TopNavProps): React.JSX.Element;
@@ -1,8 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { withActiveRouteState } from "./routing.js";
3
- export function TopNav({ siteTitle, navigation, brandHref = "/", rightSlot, currentPath }) {
3
+ export function TopNav({ siteTitle, navigation, brandHref = "/", rightSlot, currentPath, layout }) {
4
4
  const activeNavigation = currentPath
5
5
  ? withActiveRouteState(navigation, currentPath)
6
6
  : navigation.map((item) => ({ ...item, isActive: false }));
7
- return (_jsx("header", { className: "cedros-site__header", children: _jsxs("div", { className: "cedros-site__container cedros-site__header-inner", children: [_jsx("a", { href: brandHref, className: "cedros-site__brand", children: siteTitle }), _jsx("nav", { className: "cedros-site__nav", "aria-label": "Primary", children: activeNavigation.map((item) => (_jsx("a", { href: item.route, className: `cedros-site__nav-link${item.isActive ? " cedros-site__nav-link--active" : ""}`, "aria-current": item.isActive ? "page" : undefined, children: item.label }, item.key))) }), rightSlot && _jsx("div", { className: "cedros-site__header-right", children: rightSlot })] }) }));
7
+ const headerClasses = buildHeaderClasses(layout);
8
+ const linkStyleClass = layout?.linkStyle === "text" ? " cedros-site__nav-link--text" : "";
9
+ return (_jsx("header", { className: headerClasses, children: _jsxs("div", { className: "cedros-site__container cedros-site__header-inner", children: [_jsx("a", { href: brandHref, className: "cedros-site__brand", children: siteTitle }), _jsx("nav", { className: "cedros-site__nav", "aria-label": "Primary", children: activeNavigation.map((item) => (_jsx("a", { href: item.route, className: `cedros-site__nav-link${linkStyleClass}${item.isActive ? " cedros-site__nav-link--active" : ""}`, "aria-current": item.isActive ? "page" : undefined, children: item.label }, item.key))) }), rightSlot && _jsx("div", { className: "cedros-site__header-right", children: rightSlot })] }) }));
10
+ }
11
+ function buildHeaderClasses(layout) {
12
+ let classes = "cedros-site__header";
13
+ if (layout?.width === "full")
14
+ classes += " cedros-site__header--full";
15
+ if (layout?.linkPosition === "center")
16
+ classes += " cedros-site__header--nav-center";
17
+ return classes;
8
18
  }
@@ -1,6 +1,6 @@
1
- export { SiteLayout, type SiteLayoutProps, type SiteNavigationItem } from "./SiteLayout.js";
2
- export { TopNav, type TopNavProps } from "./TopNav.js";
3
- export { SiteFooter, type SiteFooterProps, type SiteFooterLink } from "./SiteFooter.js";
1
+ export { SiteLayout, type SiteLayoutProps, type SiteLayoutOptions, type SiteNavigationItem } from "./SiteLayout.js";
2
+ export { TopNav, type TopNavProps, type NavLayoutOptions } from "./TopNav.js";
3
+ export { SiteFooter, type SiteFooterProps, type SiteFooterLink, type FooterLayoutOptions } from "./SiteFooter.js";
4
4
  export { DashboardShell, type DashboardShellProps, type DashboardNavItem } from "./DashboardShell.js";
5
5
  export { DashboardOverviewTemplate, type DashboardOverviewTemplateProps, type DashboardPanel, type DashboardStat } from "./DashboardOverviewTemplate.js";
6
6
  export { isRouteActive, normalizeRoutePath, withActiveRouteState, type RouteAwareItem, type RouteMatcherOptions } from "./routing.js";
@@ -27,14 +27,14 @@
27
27
  }
28
28
 
29
29
  .cedros-dark .cedros-site {
30
- --cds-bg: hsl(222.2, 84%, 4.9%);
31
- --cds-fg: hsl(210, 40%, 98%);
32
- --cds-muted: hsl(215, 20.2%, 65.1%);
33
- --cds-muted-bg: hsl(217.2, 32.6%, 17.5%);
34
- --cds-border: hsl(217.2, 32.6%, 17.5%);
35
- --cds-link: hsl(217.2, 91.2%, 59.8%);
36
- --cds-primary: hsl(210, 40%, 98%);
37
- --cds-primary-fg: hsl(222.2, 47.4%, 11.2%);
30
+ --cds-bg: var(--cedros-background, hsl(222.2, 84%, 4.9%));
31
+ --cds-fg: var(--cedros-foreground, hsl(210, 40%, 98%));
32
+ --cds-muted: var(--cedros-muted-foreground, hsl(215, 20.2%, 65.1%));
33
+ --cds-muted-bg: var(--cedros-muted, hsl(217.2, 32.6%, 17.5%));
34
+ --cds-border: var(--cedros-border, hsl(217.2, 32.6%, 17.5%));
35
+ --cds-link: var(--cedros-link, hsl(217.2, 91.2%, 59.8%));
36
+ --cds-primary: var(--cedros-primary, hsl(210, 40%, 98%));
37
+ --cds-primary-fg: var(--cedros-primary-foreground, hsl(222.2, 47.4%, 11.2%));
38
38
  }
39
39
 
40
40
  .cedros-site__container {
@@ -107,6 +107,50 @@
107
107
  gap: 0.45rem;
108
108
  }
109
109
 
110
+ /* -- Nav layout modifiers ------------------------------------------------- */
111
+
112
+ .cedros-site__header--full .cedros-site__container {
113
+ width: 100%;
114
+ max-width: none;
115
+ padding: 0 1.5rem;
116
+ }
117
+
118
+ .cedros-site__header--nav-center .cedros-site__nav {
119
+ flex: 1;
120
+ justify-content: center;
121
+ margin-left: 0;
122
+ }
123
+
124
+ .cedros-site__nav-link--text {
125
+ border-color: transparent;
126
+ border-radius: 0;
127
+ background: transparent;
128
+ padding: 0.45rem 0.25rem;
129
+ }
130
+
131
+ .cedros-site__nav-link--text:hover {
132
+ background: transparent;
133
+ border-color: transparent;
134
+ text-decoration: underline;
135
+ }
136
+
137
+ .cedros-site__nav-link--text.cedros-site__nav-link--active {
138
+ background: transparent;
139
+ border-color: transparent;
140
+ color: var(--cds-link);
141
+ text-decoration: underline;
142
+ }
143
+
144
+ /* -- Footer layout modifiers ---------------------------------------------- */
145
+
146
+ .cedros-site__footer--full .cedros-site__container {
147
+ width: 100%;
148
+ max-width: none;
149
+ padding: 0 1.5rem;
150
+ }
151
+
152
+ /* ------------------------------------------------------------------------- */
153
+
110
154
  .cedros-site__main {
111
155
  padding: 1.5rem 0 2.4rem;
112
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedros/data-react",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "React components, page templates, and Next.js integration for cedros-data",
5
5
  "type": "module",
6
6
  "main": "./dist/react/index.js",