@cedros/data-react 0.1.5 → 0.1.7

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 (38) hide show
  1. package/README.md +108 -6
  2. package/dist/admin/icons.d.ts +1 -0
  3. package/dist/admin/icons.js +2 -1
  4. package/dist/admin/plugin.js +11 -1
  5. package/dist/admin/sectionIds.d.ts +2 -1
  6. package/dist/admin/sectionIds.js +4 -2
  7. package/dist/admin/sections/LayoutSection.d.ts +2 -0
  8. package/dist/admin/sections/LayoutSection.js +104 -0
  9. package/dist/admin/styles.css +11 -11
  10. package/dist/react/CedrosDataProvider.d.ts +28 -0
  11. package/dist/react/CedrosDataProvider.js +76 -0
  12. package/dist/react/contentCollections.d.ts +4 -0
  13. package/dist/react/contentCollections.js +48 -0
  14. package/dist/react/entries.js +6 -7
  15. package/dist/react/index.d.ts +2 -0
  16. package/dist/react/index.js +1 -0
  17. package/dist/react/sitemap.js +7 -20
  18. package/dist/react/slugs.js +7 -20
  19. package/dist/react/theme.d.ts +33 -0
  20. package/dist/react/theme.js +1 -0
  21. package/dist/site-templates/DocsTemplates.d.ts +9 -8
  22. package/dist/site-templates/DocsTemplates.js +32 -17
  23. package/dist/site-templates/SiteFooter.d.ts +7 -1
  24. package/dist/site-templates/SiteFooter.js +5 -2
  25. package/dist/site-templates/SiteLayout.d.ts +9 -1
  26. package/dist/site-templates/SiteLayout.js +2 -2
  27. package/dist/site-templates/TopNav.d.ts +11 -1
  28. package/dist/site-templates/TopNav.js +12 -2
  29. package/dist/site-templates/contentIndex.d.ts +4 -0
  30. package/dist/site-templates/contentIndex.js +15 -1
  31. package/dist/site-templates/docsNavigation.d.ts +30 -1
  32. package/dist/site-templates/docsNavigation.js +132 -1
  33. package/dist/site-templates/docsTemplateShell.d.ts +8 -0
  34. package/dist/site-templates/docsTemplateShell.js +14 -0
  35. package/dist/site-templates/index.d.ts +4 -4
  36. package/dist/site-templates/index.js +1 -1
  37. package/dist/site-templates/styles.css +52 -8
  38. package/package.json +1 -1
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`
@@ -146,13 +217,18 @@ Tipping:
146
217
  Docs templates:
147
218
  - `DocsIndexTemplate`
148
219
  - `DocArticleTemplate` (GitBook-style left nav + right TOC)
220
+ - both accept `headless` to skip the built-in `SiteLayout`, or `renderLayout={(content) => ...}` to wrap the docs UI in an existing app shell
149
221
 
150
222
  Content rendering and helpers:
151
223
  - `MarkdownContent`
152
224
  - `Breadcrumbs`
153
225
  - `ContentPagination`
154
226
  - `withActiveRouteState`
227
+ - `fetchDocEntry`
155
228
  - `buildDocsSidebarSections`
229
+ - `buildDocsTree`
230
+ - `buildHierarchicalDocsSidebarSections`
231
+ - `buildDocsPrevNext`
156
232
  - `withActiveDocsSidebar`
157
233
  - `prepareBlogIndex`
158
234
  - `prepareDocsIndex`
@@ -248,6 +324,32 @@ Docs/blog templates default to `bodyMarkdown`.
248
324
 
249
325
  This keeps markdown as the safe default and avoids unsafe HTML rendering by default.
250
326
 
327
+ Docs search helpers also inspect optional `bodyMarkdown`, `bodyText`, `bodyHtml`, and `searchText` fields on docs entries, so callers can opt into body/full-text matching without changing the query API.
328
+
329
+ ## Server helpers and API key
330
+
331
+ `@cedros/data-react/server` provides Next.js data-fetching helpers that call cedros-data directly:
332
+
333
+ ```tsx
334
+ import { generatePageMetadata, loadSitemapEntries, fetchBlogPost } from "@cedros/data-react/server";
335
+
336
+ // All helpers accept ServerFetchOptions:
337
+ const meta = await generatePageMetadata("about", {
338
+ serverUrl: process.env.CEDROS_DATA_URL,
339
+ apiKey: process.env.CEDROS_X_API_KEY, // optional, sent as x-api-key header
340
+ });
341
+ ```
342
+
343
+ The API key resolves from (in order):
344
+ 1. `options.apiKey` if explicitly passed
345
+ 2. `process.env.CEDROS_X_API_KEY` environment variable
346
+ 3. No header sent (unauthenticated — works with direct cedros-data access)
347
+
348
+ Environment variables for server URL resolution:
349
+ 1. `options.serverUrl` if explicitly passed
350
+ 2. `CEDROS_DATA_URL`
351
+ 3. `NEXT_PUBLIC_BACKEND_API_URL`
352
+
251
353
  ## Default page contract
252
354
 
253
355
  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
+ }
@@ -0,0 +1,4 @@
1
+ import type { ContentType } from "./types.js";
2
+ export declare function collectionNameForContentType(contentType: ContentType): string;
3
+ export declare function collectionNamesForContentType(contentType: ContentType): string[];
4
+ export declare function queryEntriesByContentType<T>(serverUrl: string, contentType: ContentType, buildBody: (collectionName: string) => Record<string, unknown>, apiKey?: string): Promise<T>;
@@ -0,0 +1,48 @@
1
+ import { fetchJson } from "./fetch.js";
2
+ const CANONICAL_CONTENT_TYPE_COLLECTIONS = {
3
+ page: "pages",
4
+ blog: "blog",
5
+ docs: "docs",
6
+ learn: "learn",
7
+ project: "projects",
8
+ airdrop: "airdrops",
9
+ };
10
+ const CONTENT_TYPE_COLLECTION_ALIASES = {
11
+ blog: ["blogs"],
12
+ learn: ["courses"],
13
+ };
14
+ export function collectionNameForContentType(contentType) {
15
+ return CANONICAL_CONTENT_TYPE_COLLECTIONS[contentType];
16
+ }
17
+ export function collectionNamesForContentType(contentType) {
18
+ const canonicalName = collectionNameForContentType(contentType);
19
+ const aliases = CONTENT_TYPE_COLLECTION_ALIASES[contentType] ?? [];
20
+ return [canonicalName, ...aliases.filter((alias) => alias !== canonicalName)];
21
+ }
22
+ export async function queryEntriesByContentType(serverUrl, contentType, buildBody, apiKey) {
23
+ return queryEntriesByCollectionNames(serverUrl, collectionNamesForContentType(contentType), buildBody, apiKey);
24
+ }
25
+ async function queryEntriesByCollectionNames(serverUrl, collectionNames, buildBody, apiKey) {
26
+ let lastCollectionNotFoundError;
27
+ const lastCollectionName = collectionNames[collectionNames.length - 1];
28
+ for (const collectionName of collectionNames) {
29
+ try {
30
+ return await fetchJson(serverUrl, "/entries/query", {
31
+ method: "POST",
32
+ body: buildBody(collectionName),
33
+ apiKey,
34
+ });
35
+ }
36
+ catch (error) {
37
+ if (!isCollectionNotFoundError(error) || collectionName === lastCollectionName) {
38
+ throw error;
39
+ }
40
+ lastCollectionNotFoundError = error;
41
+ }
42
+ }
43
+ throw (lastCollectionNotFoundError ??
44
+ new Error("@cedros/data-react: no collection names were configured"));
45
+ }
46
+ function isCollectionNotFoundError(error) {
47
+ return error instanceof Error && /collection not found/i.test(error.message);
48
+ }
@@ -1,4 +1,5 @@
1
- import { fetchJson, resolveApiKey, resolveServerUrl } from "./fetch.js";
1
+ import { resolveApiKey, resolveServerUrl } from "./fetch.js";
2
+ import { queryEntriesByContentType } from "./contentCollections.js";
2
3
  /**
3
4
  * Fetches a single blog post by slug, optionally passing visitor_id for metered reads.
4
5
  *
@@ -10,7 +11,6 @@ export async function fetchBlogPost(slug, options) {
10
11
  const serverUrl = resolveServerUrl(options);
11
12
  const apiKey = resolveApiKey(options);
12
13
  const body = {
13
- collection_name: "blog",
14
14
  entry_keys: [slug],
15
15
  limit: 1,
16
16
  offset: 0,
@@ -18,10 +18,9 @@ export async function fetchBlogPost(slug, options) {
18
18
  if (options?.visitorId) {
19
19
  body.visitor_id = options.visitorId;
20
20
  }
21
- const entries = await fetchJson(serverUrl, "/entries/query", {
22
- method: "POST",
23
- body,
24
- apiKey,
25
- });
21
+ const entries = await queryEntriesByContentType(serverUrl, "blog", (collectionName) => ({
22
+ ...body,
23
+ collection_name: collectionName,
24
+ }), apiKey);
26
25
  return entries.length > 0 ? entries[0] : null;
27
26
  }
@@ -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";
@@ -1,13 +1,5 @@
1
- import { fetchJson, resolveApiKey, resolveServerUrl } from "./fetch.js";
2
- /** Map of content types to their collection names in cedros-data. */
3
- const CONTENT_TYPE_COLLECTIONS = {
4
- page: "pages",
5
- blog: "blog",
6
- docs: "docs",
7
- learn: "learn",
8
- project: "projects",
9
- airdrop: "airdrops",
10
- };
1
+ import { resolveApiKey, resolveServerUrl } from "./fetch.js";
2
+ import { queryEntriesByContentType } from "./contentCollections.js";
11
3
  /** Default change frequency and priority for each content type. */
12
4
  const CONTENT_TYPE_DEFAULTS = {
13
5
  page: { changeFrequency: "monthly", priority: 0.8 },
@@ -67,17 +59,12 @@ export async function loadSitemapEntries(options) {
67
59
  return entries;
68
60
  }
69
61
  async function fetchCollectionSlugs(serverUrl, contentType, apiKey) {
70
- const collectionName = CONTENT_TYPE_COLLECTIONS[contentType];
71
62
  const defaults = CONTENT_TYPE_DEFAULTS[contentType];
72
- const records = await fetchJson(serverUrl, "/entries/query", {
73
- method: "POST",
74
- body: {
75
- collection_name: collectionName,
76
- limit: 1000,
77
- offset: 0,
78
- },
79
- apiKey,
80
- });
63
+ const records = await queryEntriesByContentType(serverUrl, contentType, (collectionName) => ({
64
+ collection_name: collectionName,
65
+ limit: 1000,
66
+ offset: 0,
67
+ }), apiKey);
81
68
  return records.map((record) => {
82
69
  const slug = record.payload.slug ??
83
70
  record.payload.route?.replace(/^\//, "") ??