@dalgoridim/headless-cms 0.2.0 → 0.2.1

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/ARCHITECTURE.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # Architecture & design notes
2
2
 
3
3
  Core/internal documentation for `@dalgoridim/headless-cms`. For consumer usage,
4
- see the [README](./README.md). For the history of the headless redesign, see
5
- [REDESIGN.md](./REDESIGN.md).
4
+ see the [README](./README.md).
6
5
 
7
6
  ## Philosophy: harness, not framework
8
7
 
package/README.md CHANGED
@@ -13,6 +13,11 @@ You build a thin skin over the primitives (see [Styling](#styling-bring-your-own
13
13
  the package never pushes a look onto your site. The design rationale and internals
14
14
  live in [ARCHITECTURE.md](./ARCHITECTURE.md).
15
15
 
16
+ **Live example:** [dalgoridim.com](https://dalgoridim.com) runs on this package.
17
+ Try it: anyone can toggle edit mode and change the content inline, right on the
18
+ page — but saves are gated, so only the authenticated owner's edits actually
19
+ persist. Your changes are yours alone to play with.
20
+
16
21
  ## Install
17
22
 
18
23
  ```bash
@@ -353,7 +358,6 @@ without ever changing how your site looks.
353
358
  - **[README](./README.md)** — this guide: install and usage.
354
359
  - **[ARCHITECTURE.md](./ARCHITECTURE.md)** — design rationale and internals
355
360
  (bundle boundaries, backend parity, the hybrid Postgres model, headless UI).
356
- - **[REDESIGN.md](./REDESIGN.md)** — the "harness, not framework" redesign spec.
357
361
 
358
362
  ## Build
359
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalgoridim/headless-cms",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Database-agnostic, inline-edit headless CMS engine for React / Next.js apps",
5
5
  "license": "UNLICENSED",
6
6
  "author": "dalgoridim",
@@ -96,13 +96,14 @@
96
96
  },
97
97
  "files": [
98
98
  "dist",
99
- "ARCHITECTURE.md",
100
- "REDESIGN.md"
99
+ "ARCHITECTURE.md"
101
100
  ],
102
101
  "scripts": {
103
102
  "build": "tsup",
104
103
  "dev": "tsup --watch",
105
104
  "typecheck": "tsc --noEmit",
105
+ "test": "vitest run",
106
+ "test:watch": "vitest",
106
107
  "prepare": "tsup"
107
108
  },
108
109
  "peerDependencies": {
@@ -155,6 +156,7 @@
155
156
  "react": "^19.0.0",
156
157
  "react-dom": "^19.0.0",
157
158
  "tsup": "^8.5.1",
158
- "typescript": "^5.7.0"
159
+ "typescript": "^5.7.0",
160
+ "vitest": "^3.0.0"
159
161
  }
160
162
  }
package/REDESIGN.md DELETED
@@ -1,250 +0,0 @@
1
- # Headless CMS — "Harness, Not Framework" Redesign
2
-
3
- > Full spec for the "harness, not framework" redesign. Living document — kept in
4
- > sync as the two tracks land.
5
-
6
- ## Context
7
-
8
- `@dalgoridim/headless-cms` was extracted from `portfolio-2025`. An audit of what
9
- the package *forces* on its consumers found two layers with opposite philosophies:
10
-
11
- - **Data / server layer — already free.** `Section` is schemaless
12
- (`{ id, collection, [key]: any }`), the Postgres adapter never drops unmapped
13
- fields (JSONB `documents` fallback + `extra` column), and `DataAdapter` is
14
- generic. Good.
15
- - **UI / client layer — a framework, not a harness.** The components ship a
16
- finished, portfolio-specific look and hard dependencies. A consumer adopting
17
- the package today **inherits portfolio-2025's visual design** and is forced
18
- into Tailwind, a dark `neutral` palette, a `primary` design token,
19
- `lucide-react`, `sonner`, and a bespoke text-mark syntax.
20
-
21
- The goal: make the **whole** package a harness — wire behavior, impose nothing.
22
- Two tracks, shipped together:
23
-
24
- - **Track A** — de-framework-ize the UI (the philosophy fix; highest impact).
25
- - **Track B** — close the four data-layer capability gaps (query power,
26
- relations, loosened `id`/`collection`, auth flexibility).
27
-
28
- ### Decisions locked in with the user
29
- - Package goes **pure headless**; the current dark/Tailwind look is **moved into
30
- portfolio-2025's own shim components**, not kept in the package.
31
- - API optimized for **DX**: `className`/`style` passthrough + `data-*` state
32
- attributes + injectable deps (icons / notifier / markup parser); render-props
33
- / `asChild` only where it genuinely improves control.
34
- - **Both tracks in one pass.**
35
-
36
- ### Guiding principles (apply to every change)
37
- 1. Ship **unstyled** by default — no Tailwind, no palette, no `primary` token.
38
- 2. Expose **styling hooks** — `className`, `style`, `data-*` state attributes,
39
- and render-props/`asChild` for complex chrome.
40
- 3. Make deps **injectable, not baked** — icons, toasts, markup parser.
41
- 4. **No portfolio-specific code** in the package.
42
- 5. Never silently break portfolio — package + portfolio shims change in lockstep.
43
-
44
- ---
45
-
46
- ## Current forcing inventory (what we are removing)
47
-
48
- | Forced thing | Location | Resolution |
49
- |---|---|---|
50
- | Tailwind mandatory (`cn`, utility strings) | all client components, `client/utils.ts` | Strip utility strings; `cn`/`tailwind-merge` leaves core. |
51
- | Dark `neutral` palette + ring offsets | `ContentEditSpan`, `EditableImage`, `MarkdownEditor` | Move to portfolio shims. |
52
- | `primary` token (`var(--color-primary)`, `bg-primary`) | `ContentEditSpan.tsx:103`, `EditableImage`, `MarkdownEditor` | Removed from package; portfolio keeps it in its shims. |
53
- | `lucide-react` + `sonner` baked in | `EditableImage`, `MarkdownEditor`, `PageProvider` default notifier | Drop from hard deps; inject; portfolio supplies them. |
54
- | Bespoke text-mark syntax (`^^primary^^`, `~~br~~`, `__underline__`) | `ContentEditSpan.tsx:30` `PATTERNS` | Make parser/renderer injectable with a default. |
55
- | Portfolio leftovers: `collection = "portfolio"` default, `ProjectContentEditor` | `ContentEditSpan.tsx:151`, `MarkdownEditor.tsx:293` | Delete from package; move `ProjectContentEditor` to portfolio. |
56
- | Hardcoded English copy | `EditableImage`, `MarkdownEditor` | Replace with props/children defaults. |
57
- | `id` + `collection` forced on content type | `types.ts:12` | Split internal addressing from user's `T` (Track B3). |
58
- | Tiny query language | `types.ts:48` | Extend neutrally (Track B1). |
59
- | `AuthIdentity` forces `userId`+`isAdmin` | `types.ts:92` | Minimal contract + open payload + `authorize` predicate (Track B4). |
60
-
61
- ---
62
-
63
- ## Track A — Headless UI
64
-
65
- ### A1. `PageProvider` (`src/client/PageProvider.tsx`)
66
- - `Notifier` is already injectable — keep. **Change the default** from a `sonner`
67
- toast sink to a dependency-free console/no-op default so `sonner` is no longer
68
- required. Portfolio passes a sonner-backed notifier from its shim.
69
- - No other behavior change; this file is mostly backend-agnostic already.
70
-
71
- ### A2. `ContentEditSpan` (`src/client/ContentEditSpan.tsx`)
72
- - Remove `collection = "portfolio"` default → `collection` becomes required (or
73
- resolved from an optional provider-level default-collection context).
74
- - Extract the `PATTERNS` parser + `RenderStatic` into an **injectable markup
75
- contract**: `parse(raw) => Node[]` + `render(nodes)`, defaulted to the current
76
- implementation but overridable via prop or a `CmsMarkupContext`. Consumers who
77
- want plain text or their own DSL drop in their own.
78
- - Strip all Tailwind/`primary`/`neutral` classes. Keep `className` passthrough.
79
- Emit **state via data attributes**: `data-cms-editable`, `data-editing`,
80
- `data-focused` so consumers style with their own CSS.
81
- - Keep the `as` prop; add `asChild` (lightweight Slot) so the consumer can supply
82
- their own element/markup while the primitive wires `contentEditable`.
83
-
84
- ### A3. `EditableImage` (`src/client/EditableImage.tsx`)
85
- - Keep the upload/url/pending-image behavior (it's correct and engine-wired).
86
- - Remove `lucide` icons and all dark modal/overlay styling. Expose a
87
- **render-prop API**: the component manages state and hands the consumer
88
- `{ src, isEditing, saving, openFilePicker, setUrl, error }`; the consumer
89
- renders the overlay/modal. Provide a minimal **unstyled default** render so the
90
- simple case still works without writing chrome.
91
- - Placeholder ("No image available") and modal copy become props/children.
92
-
93
- ### A4. `MarkdownEditor` (`src/client/MarkdownEditor.tsx`)
94
- - This is the most styled file. Split into:
95
- - **Headless core**: editor state + commands (`insertMarkdown`, preview
96
- toggle, save/cancel, char count) exposed via hook/render-prop.
97
- - The modal chrome, toolbar, icons (`lucide`), markdown guide, and
98
- `react-markdown`/`remark-gfm` preview move to **portfolio**.
99
- - `react-markdown` + `remark-gfm` leave the package's hard deps (preview is a
100
- consumer concern; the core only manages text).
101
- - **Delete `ProjectContentEditor`** from the package (portfolio-specific) → move
102
- to portfolio.
103
-
104
- ### A5. `client/utils.ts` + deps
105
- - `cn` relies on `tailwind-merge` (Tailwind-specific). Drop `tailwind-merge` from
106
- the package; either remove `cn` from the public API or reduce it to a plain
107
- `clsx` join. Tailwind-specific merging belongs in portfolio.
108
-
109
- ### A6. `package.json` dependency surgery
110
- Move out of hard `dependencies` (→ removed from package; portfolio installs):
111
- `lucide-react`, `react-markdown`, `remark-gfm`, `sonner`, `tailwind-merge`.
112
- Keep only genuinely shared, non-opinion runtime deps (`clsx` at most). Update
113
- `client/index.ts` exports (remove `ProjectContentEditor`; keep `cn` only if
114
- retained).
115
-
116
- ---
117
-
118
- ## Track B — Data-layer freedom
119
-
120
- Touch points: `src/types.ts`, `src/adapters/postgres/index.ts`,
121
- `src/adapters/firestore/index.ts` (read during impl), `src/server/*`.
122
-
123
- ### B1. Query power (`types.ts:48`, both adapters)
124
- Extend the neutral `Query` without leaking any backend type:
125
- - Ops: add `ne`, `nin`, `contains` (case-insensitive substring → SQL `ILIKE
126
- %v%`), keep existing.
127
- - **OR groups**: allow `filters` to nest — an `or: Filter[]` group alongside the
128
- implicit top-level `AND`.
129
- - **Pagination**: add `offset` next to `limit`.
130
- - Implement fully in Postgres (`colExpr`/`OP_MAP` already structured for it).
131
- Implement best-effort in Firestore and **throw a clear error** on ops the
132
- backend can't honor (e.g. cross-field `OR`, `contains`) rather than returning
133
- wrong results. Document the parity matrix.
134
-
135
- ### B2. Relations (schemaless-friendly)
136
- - A reference is just a field holding an id (or `{ collection, id }`) or an array
137
- of them — no schema change required to store one.
138
- - Add an optional `populate?: string[]` to `Query` and a neutral resolver in the
139
- engine that, after the primary fetch, **batch-loads referenced docs** via an
140
- `in` query and inlines them. Backend-agnostic (works on JSONB + Firestore).
141
- - Optional `RelationConfig` (field → target collection) so refs that are bare ids
142
- know where to resolve.
143
-
144
- ### B3. Loosen `id` / `collection` (`types.ts:12`)
145
- - Keep `id`/`collection` as the engine's **internal addressing**, but stop
146
- forcing them onto the user's content type. Introduce `Editable<T> = T & {
147
- id: string; collection: string }` used internally; public APIs accept the
148
- user's clean `T`.
149
- - Make the field names **configurable** at the adapter level (`idField`,
150
- `collectionField`) for consumers whose records use `_id` / `slug`.
151
-
152
- ### B4. Auth flexibility (`types.ts:92`, `server/createAdminGate.ts`)
153
- - `AuthIdentity` → minimal `{ isAdmin: boolean }` plus an open payload
154
- (`[key: string]: unknown`; `userId`/`email` optional).
155
- - `verifyRequest` may return any identity shape; the gate authorizes via an
156
- injectable `authorize(identity, req) => boolean` predicate, **defaulting** to
157
- `identity.isAdmin === true`. Lets consumers gate on roles/scopes without the
158
- fixed contract.
159
- - Client `CmsAuthState` stays minimal but becomes extensible.
160
-
161
- ---
162
-
163
- ## Portfolio-2025 migration (lockstep)
164
-
165
- Portfolio consumes the package only through **thin shims**, so the look moves
166
- there cleanly:
167
-
168
- - `lib/context/PageContent.tsx` — pass a **sonner-backed `notify`** to
169
- `PageProvider` (restores toasts).
170
- - `components/customs/ContentEditSpan.tsx` — wrap the headless primitive,
171
- re-apply the dark ring/`primary` Tailwind classes and the default `collection`.
172
- - `components/customs/EditableImage.tsx` — supply the styled overlay/modal +
173
- `lucide` icons via the render-prop.
174
- - `components/customs/MarkdownEditor.tsx` — supply the modal chrome, toolbar,
175
- `lucide` icons, `react-markdown` preview, guide; **define `ProjectContentEditor`
176
- here**.
177
- - Install in portfolio what left the package: `lucide-react`, `sonner`,
178
- `react-markdown`, `remark-gfm`, `tailwind-merge` (most already present).
179
- - `lib/cms/server.ts` / auth shim — adjust only if `AuthIdentity` field changes
180
- ripple (expected: none, contract is widened not narrowed).
181
-
182
- Portfolio's `app/globals.css` already defines `--primary` / `--color-primary`,
183
- so its shims keep the exact current look.
184
-
185
- ---
186
-
187
- ## Sequencing (so nothing breaks mid-flight)
188
-
189
- 1. **Track B** first (additive, low-risk): `types.ts` Query/Auth widening, then
190
- Postgres + Firestore adapter impls. Widening contracts won't break portfolio.
191
- 2. **Track A** package changes: provider default notifier, headless
192
- ContentEditSpan / EditableImage / MarkdownEditor, dep surgery, exports.
193
- 3. **Portfolio shims** updated in the same change set to restore look + behavior.
194
- 4. Drop this doc into the package root as `REDESIGN.md`.
195
-
196
- ---
197
-
198
- ## Verification
199
-
200
- - **Package**: `npm run build` (tsup) and `npm run typecheck` in
201
- `headless-cms` — both clean.
202
- - **Portfolio typecheck/build**: `npm run build` in `portfolio-2025` resolves all
203
- shims against the new headless API.
204
- - **Postgres proof** (also a memory milestone): run `scripts/seed-postgres.ts`,
205
- then exercise new query ops (`contains`, `or`, `offset`) and a `populate`
206
- round-trip against the local `documents` table.
207
- - **Manual UI** (`/verify` or `/run`): in portfolio admin/edit mode, confirm
208
- inline text edit, image upload + external-URL, and the markdown modal all still
209
- look and behave as before — proving the look fully survived the move to shims.
210
- - **Headless smoke**: render `ContentEditSpan` / `EditableImage` with **no**
211
- Tailwind/styles to confirm they work unstyled (harness proof).
212
-
213
- ## Open risks
214
- - **Firestore query parity** — it cannot honor cross-field `OR` / `contains`
215
- generally. Plan: throw explicit "unsupported by this adapter" errors and ship a
216
- documented parity matrix rather than silent wrong results.
217
- - **`asChild`/render-prop surface** — adds API; mitigated by keeping unstyled
218
- defaults so trivial cases stay one-liners.
219
-
220
- ---
221
-
222
- ## Status (landed)
223
-
224
- - **Track B1 (query power)** — done. `QueryFilterOp` gains `ne`/`nin`/`contains`;
225
- `Query` gains OR groups (`{ or: [...] }`) and `offset`. Postgres implements all;
226
- Firestore throws clear errors on `contains` / OR groups.
227
- - **Track B2 (relations)** — done. `resolveRelations(adapter, docs, opts)` in
228
- `src/server/relations.ts` (exported from `/server`); `Query.populate`,
229
- `Ref`, `RelationConfig` added.
230
- - **Track B3 (id/collection)** — **type-level done** (`Editable<T>` +
231
- `EntityAddress`; `T` unconstrained). **Deferred:** per-adapter configurable
232
- physical `idField`/`collectionField` renaming — bigger SQL change, left as a
233
- follow-up to avoid destabilizing the working schemaless path.
234
- - **Track B4 (auth)** — done. `AuthIdentity` widened to `{ isAdmin } + open
235
- payload`; injectable `AuthorizeFn` on `createAdminGate` / `createCmsHandlers`.
236
- - **Track A (headless UI)** — done. `ContentEditSpan` (injectable `renderValue`,
237
- `data-cms-*` attrs, no styles), `EditableImage` (render-prop), `MarkdownEditor`
238
- → `useMarkdownEditor` hook. Removed `ProjectContentEditor`, `cn`/`utils.ts`,
239
- and all of `lucide-react`/`sonner`/`react-markdown`/`remark-gfm`/`tailwind-merge`
240
- from package deps (now **zero** runtime deps).
241
- - **Portfolio shims** — restyled to restore the exact look: `ContentEditSpan`,
242
- `EditableImage`, `MarkdownEditor`+`ProjectContentEditor`, sonner `notify`.
243
-
244
- Verified: package `typecheck` + `build` clean; portfolio `tsc --noEmit` clean
245
- against the new API; portfolio **production build passes** (firebase mode, all
246
- routes); **Postgres round-trip 12/12** — `contains`, OR groups, `ne`/`nin`,
247
- `offset`+order (text + typed numeric), and `populate` via `RelationConfig`.
248
- Caveat surfaced: numeric fields in the schemaless JSONB `documents` table order
249
- as text (`data->>` is text); register a typed collection for true numeric
250
- ordering/comparison. Not yet run: manual edit-mode UI pass (needs admin login).