@odla-ai/blog 0.0.2 → 0.0.3

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
@@ -33,9 +33,13 @@ Four bundled themes, all with light + dark modes (system preference + ☾/☀ to
33
33
  - **chalk** — chalkboard on graph paper; copper/cerulean, mono chrome
34
34
  - **clay** — sand paper and terracotta; system fonts only, zero external requests
35
35
 
36
- Pick one with `"theme": "salt"` in `blog.config.json`. Override any theme file by
37
- placing a same-named file in your site's `theme/` directory. Preview all four
38
- against the example site with `npm run preview` (from the monorepo root).
36
+ Pick one with `"theme": "salt"` in `blog.config.json`. Themes ship in the
37
+ [`@odla-ai/ui`](https://www.npmjs.com/package/@odla-ai/ui) dependency (the odla
38
+ design system); every built site also gets ui's class-scoped component layer
39
+ (`assets/odla-ui.css` — buttons, form fields, cards, …) theme-matched via the
40
+ theme's `ui.css` token aliases. Override any theme file by placing a
41
+ same-named file in your site's `theme/` directory. Preview all four against
42
+ the example site with `npm run preview` (from the monorepo root).
39
43
 
40
44
  ## Deploy (Cloudflare)
41
45
 
package/llms.txt CHANGED
@@ -109,12 +109,13 @@ const result = await build({
109
109
 
110
110
  Output: `index.html`, `posts/<slug>/index.html`, `<page>/index.html`,
111
111
  `archive/`, `tags/`, `tags/<tag>/`, `rss.xml`, `sitemap.xml`,
112
- `assets/styles.css`, plus everything from `public/`. All URLs are prefixed
113
- with `config.basePath` (default `/`) — set it when deploying under a subpath
114
- (e.g. GitHub project pages).
112
+ `assets/styles.css`, `assets/odla-ui.css`, plus everything from `public/`.
113
+ All URLs are prefixed with `config.basePath` (default `/`) — set it when
114
+ deploying under a subpath (e.g. GitHub project pages).
115
115
 
116
116
  Also exported: `loadPosts`, `loadPages`, `parseFrontmatter`, `slugFromFilename`,
117
- `renderMarkdown`, `slugify`, `escapeHtml`, `resolveThemeFile`.
117
+ `renderMarkdown`, `slugify`, `escapeHtml`, `resolveThemeFile`, `listThemes`,
118
+ `THEME_NAMES`.
118
119
 
119
120
  ## Markdown
120
121
 
@@ -125,13 +126,19 @@ no client-side JS. Headings get slugified `id` anchors.
125
126
  ## Theming
126
127
 
127
128
  Bundled themes: `juniper` (default), `salt`, `chalk`, `clay` — pick with
128
- `"theme": "<name>"` in config. Each theme is a single self-contained
129
- `src/themes/<name>/styles.css` plus `theme.json` metadata.
130
-
131
- Invariants every theme upholds (keep them when editing or adding one):
129
+ `"theme": "<name>"` in config. **Themes live in the `@odla-ai/ui`
130
+ dependency** (`node_modules/@odla-ai/ui/themes/<name>/`): `styles.css` (the
131
+ full theme), `ui.css` (maps the theme's palette onto the `--ui-*` design
132
+ tokens), `theme.json` (metadata). Resolution order for any theme file:
133
+ `<site>/theme/<file>` → `@odla-ai/ui/themes/<name>/<file>` →
134
+ `src/templates/<file>`. (`@odla-ai/ui` also ships a `paper` theme — it is a
135
+ dashboard theme, NOT a blog theme; it doesn't style the selector contract.)
136
+
137
+ Invariants every theme upholds (keep them when editing or adding one;
138
+ tested in @odla-ai/ui's `test/themes.test.js`):
132
139
  - Light tokens on `:root`; dark tokens duplicated on BOTH `[data-theme="dark"]`
133
140
  and `@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) }`.
134
- The two dark blocks must stay identical (tested in `test/themes.test.js`).
141
+ The two dark blocks must stay identical.
135
142
  - Common token names: `--bg`, `--text`, `--accent`, `--border`, `--code-bg`,
136
143
  and the highlight set `--hl-keyword`, `--hl-string`, `--hl-comment`,
137
144
  `--hl-number`, `--hl-title`, `--hl-attr`, `--hl-type`, `--hl-meta`,
@@ -141,15 +148,25 @@ Invariants every theme upholds (keep them when editing or adding one):
141
148
  `.post-title`, `.post-meta`, `.post-tag`, `.prose`, `.page-title`,
142
149
  `.archive-year`, `.archive-list`, `.archive-item`, `.tag-cloud`, `.hljs-*`.
143
150
 
151
+ **Component classes on any page:** every built site also ships
152
+ `assets/odla-ui.css` (linked before the theme stylesheet) — the class-scoped
153
+ @odla-ai/ui component layer (`.btn`, `.field`/`.input`/`.textarea`, `.card`,
154
+ `.table`, `.banner`, chat classes, …), theme-correct because the build
155
+ appends the theme's `ui.css` token aliases into `assets/styles.css`. Use
156
+ these classes in pages/templates instead of inventing inline styles; the
157
+ full inventory is in `node_modules/@odla-ai/ui/llms.txt`. A site
158
+ `public/assets/odla-ui.css` overrides the shipped copy (public/ copies
159
+ last), same valve as styles.css.
160
+
144
161
  **Site-local overrides:** any file placed in the site's `theme/` directory
145
- shadows the packaged theme file of the same name (`styles.css`, or any
146
- template like `post.js` or `index.js`). Copy the packaged file out, edit,
162
+ shadows the packaged theme file of the same name (`styles.css`, `ui.css`, or
163
+ any template like `post.js` or `index.js`). Copy the packaged file out, edit,
147
164
  done. Templates are plain JS functions returning template-literal HTML
148
165
  strings; they may import shared helpers from the package
149
166
  (`import { postList } from "@odla-ai/blog"`). To ADD styles without forking
150
167
  the whole stylesheet, put them in `theme/custom.css` — the build appends it
151
- to the theme's CSS. Post URLs live under `config.postsPath` (default
152
- "posts").
168
+ to the theme's CSS (after the `ui.css` aliases). Post URLs live under
169
+ `config.postsPath` (default "posts").
153
170
 
154
171
  ## Deploy (Cloudflare)
155
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odla-ai/blog",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Minimal-dependency, static-first blogging platform. Files in, site out; odla-db lights up interactivity when configured.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@
24
24
  "test": "node --test --test-reporter=spec"
25
25
  },
26
26
  "dependencies": {
27
+ "@odla-ai/ui": "^0.1.0",
27
28
  "highlight.js": "^11.11.0",
28
29
  "marked": "^16.0.0",
29
30
  "yaml": "^2.8.0"
package/src/build.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // Static build: content + theme → dist/. Pure pipeline, top to bottom.
2
2
  //
3
3
  // Theme resolution (file-by-file shadowing):
4
- // 1. <site>/theme/<file> — site-local override
5
- // 2. src/themes/<name>/<file> — the chosen packaged theme
6
- // 3. src/templates/<file> — shared default templates
7
- // Themes usually only provide styles.css; templates come from the shared set.
4
+ // 1. <site>/theme/<file> — site-local override
5
+ // 2. @odla-ai/ui/themes/<name>/<file> — the chosen packaged theme
6
+ // 3. src/templates/<file> — shared default templates
7
+ // Themes usually only provide styles.css (+ ui.css); templates come from
8
+ // the shared set. Themes live in the @odla-ai/ui dependency.
8
9
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
10
  import { existsSync } from "node:fs";
10
11
  import { join, dirname } from "node:path";
@@ -13,15 +14,16 @@ import { loadPosts, loadPages, loadJsPages, loadCollections } from "./content.js
13
14
  import { renderMarkdown, slugify } from "./markdown.js";
14
15
  import { excerpt } from "./excerpt.js";
15
16
  import { atom, rss, sitemap } from "./feeds.js";
17
+ import { THEME_NAMES, uiFile, uiThemeFile } from "./themes.js";
16
18
 
17
19
  const SRC = dirname(fileURLToPath(import.meta.url));
18
20
 
19
21
  export function resolveThemeFile(siteDir, themeName, file) {
20
22
  const candidates = [
21
23
  join(siteDir, "theme", file),
22
- join(SRC, "themes", themeName, file),
24
+ uiThemeFile(themeName, file),
23
25
  join(SRC, "templates", file),
24
- ];
26
+ ].filter(Boolean);
25
27
  return candidates.find((c) => existsSync(c)) ?? null;
26
28
  }
27
29
 
@@ -188,17 +190,37 @@ export async function build({
188
190
  await write("atom.xml", atom({ config, posts }));
189
191
  await write("sitemap.xml", sitemap({ config, posts, pages }));
190
192
 
191
- // theme/custom.css is appended to the theme stylesheet additions without
193
+ // Theme stylesheet assembly: styles.css, then the theme's ui.css (maps its
194
+ // palette onto the @odla-ai/ui --ui-* token contract so component classes
195
+ // render theme-correct), then site theme/custom.css — additions without
192
196
  // forking the whole file (full replacement: shadow styles.css itself).
193
197
  const styles = resolveThemeFile(siteDir, themeName, "styles.css");
194
- if (!styles) throw new Error(`Theme "${themeName}" has no styles.css`);
198
+ if (!styles) {
199
+ throw new Error(
200
+ `Theme "${themeName}" has no styles.css — bundled themes: ${THEME_NAMES.join(", ")} (or add <site>/theme/styles.css)`
201
+ );
202
+ }
195
203
  let css = await readFile(styles, "utf8");
204
+ const uiAlias = resolveThemeFile(siteDir, themeName, "ui.css");
205
+ if (uiAlias) {
206
+ css += `\n/* ─── ${themeName}/ui.css (odla-ui token aliases) ─── */\n${await readFile(uiAlias, "utf8")}`;
207
+ }
196
208
  const customCss = join(siteDir, "theme", "custom.css");
197
209
  if (existsSync(customCss)) {
198
210
  css += `\n/* ─── site custom.css ─── */\n${await readFile(customCss, "utf8")}`;
199
211
  }
200
212
  await write(join("assets", "styles.css"), css);
201
213
 
214
+ // Every site ships the ui component layer (class-scoped, inert until a
215
+ // page uses the classes). base.js links it BEFORE styles.css so the theme
216
+ // and site css win ties. A site public/assets/odla-ui.css overrides it
217
+ // (public/ copies last), same valve as styles.css.
218
+ const uiCss = uiFile("odla-ui.css");
219
+ if (!uiCss) {
220
+ throw new Error("@odla-ai/ui not found — run npm install (blog themes and component CSS ship there)");
221
+ }
222
+ await write(join("assets", "odla-ui.css"), await readFile(uiCss, "utf8"));
223
+
202
224
  const publicDir = join(siteDir, "public");
203
225
  if (existsSync(publicDir)) await cp(publicDir, outDir, { recursive: true });
204
226
 
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { build, resolveThemeFile } from "./build.js";
2
+ export { listThemes, THEME_NAMES } from "./themes.js";
2
3
  export { loadPosts, loadPages, parseFrontmatter, slugFromFilename } from "./content.js";
3
4
  export { renderMarkdown, slugify, escapeHtml } from "./markdown.js";
4
5
  export { postList, tagList, tagUrl, formatDate, isoDate } from "./templates/helpers.js";
@@ -125,6 +125,7 @@ export function base({ config, title, description, path, image, content }) {
125
125
  <meta charset="utf-8">
126
126
  <meta name="viewport" content="width=device-width, initial-scale=1">
127
127
  ${headMeta(config, { fullTitle, description, path, image })}
128
+ <link rel="stylesheet" href="${base}assets/odla-ui.css">
128
129
  <link rel="stylesheet" href="${base}assets/styles.css">
129
130
  <script>${NO_FLASH}</script>
130
131
  </head>
package/src/themes.js ADDED
@@ -0,0 +1,32 @@
1
+ // Themes ship in @odla-ai/ui (themes/<name>/{styles.css,ui.css,theme.json});
2
+ // this module is the blog's bridge to them. createRequire (not
3
+ // import.meta.resolve) because it works on every Node 20.x, returns plain
4
+ // filesystem paths, and verifies existence with a catchable error.
5
+ import { createRequire } from "node:module";
6
+ import { readFileSync } from "node:fs";
7
+
8
+ const require = createRequire(import.meta.url);
9
+
10
+ // The bundled themes that uphold the blog selector contract. @odla-ai/ui
11
+ // also ships "paper", which does not — keep it out of this list.
12
+ export const THEME_NAMES = ["chalk", "clay", "juniper", "salt"];
13
+
14
+ // Absolute path of a file shipped by @odla-ai/ui, or null if absent.
15
+ export function uiFile(subpath) {
16
+ try {
17
+ return require.resolve(`@odla-ai/ui/${subpath}`);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ export function uiThemeFile(themeName, file) {
24
+ return uiFile(`themes/${themeName}/${file}`);
25
+ }
26
+
27
+ // Metadata for every bundled blog theme (for pickers/galleries).
28
+ export function listThemes() {
29
+ return THEME_NAMES.map((name) =>
30
+ JSON.parse(readFileSync(uiThemeFile(name, "theme.json"), "utf8"))
31
+ );
32
+ }