@pmeweb/content 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/dist/collection.d.ts +26 -0
- package/dist/collection.js +74 -0
- package/dist/published.d.ts +4 -0
- package/dist/published.js +14 -0
- package/dist/schedule.d.ts +10 -5
- package/dist/schedule.js +24 -5
- package/dist/types.d.ts +23 -3
- package/dist/types.js +7 -1
- package/dist/zodFrom.js +5 -4
- package/package.json +13 -3
- package/prose.css +17 -0
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ collections in `src/content.schema.json`; this package is what reads that declar
|
|
|
9
9
|
| `@pmeweb/content/zodFrom` | declaration -> Astro zod schema (needs `astro`) | sites |
|
|
10
10
|
| `@pmeweb/content/fields` | `isDate`, `isDateTime`, `isTime`, `isDayDate`, `TIME_RE` | portal tier 1, `zodFrom` |
|
|
11
11
|
| `@pmeweb/content/schedule` | `scheduleState`, `visibleOnSite`, `portalPreview`, `scheduleErrors`, `jourQuebec`, `quebecDay` | sites, portal |
|
|
12
|
+
| `@pmeweb/content/collection` | `titleFieldError`, `thumbnailError`, `richErrors`, `collectionErrors`, `schemaErrors`, `richBodyField`, `hasThumbnails` | portal, `zodFrom` |
|
|
13
|
+
| `@pmeweb/content/published` | `getPublished`, the one filter every site's pages go through | sites |
|
|
14
|
+
| `@pmeweb/content/prose.css` | the structure of a row of images in a rich body | sites' `Prose.astro` |
|
|
12
15
|
|
|
13
16
|
## Dated kinds
|
|
14
17
|
|
|
@@ -33,10 +36,31 @@ collections in `src/content.schema.json`; this package is what reads that declar
|
|
|
33
36
|
The builder sets `PMEWEB_PORTAL_PREVIEW=1` on the portal's preview builds, and
|
|
34
37
|
`PMEWEB_PORTAL_PREVIEW_PAST=1` too when the client ticked « Afficher les entrées passées ». A site
|
|
35
38
|
reads both once, `const APERCU = portalPreview(process.env)`, and filters with
|
|
36
|
-
`
|
|
37
|
-
|
|
39
|
+
`getPublished(entries, decl.<collection>, now, APERCU)`: live entries always, scheduled ones and
|
|
40
|
+
drafts in the preview, removed ones only in a preview that asked for them. Only the exact
|
|
38
41
|
string `'1'` counts, and the past flag means nothing without the preview flag.
|
|
39
42
|
|
|
43
|
+
## 2.0.0
|
|
44
|
+
|
|
45
|
+
Four declaration keys:
|
|
46
|
+
|
|
47
|
+
- `titleField` (REQUIRED): the entry's human title, a top-level `line`. Lists, reference dropdowns
|
|
48
|
+
and confirmations show it. May name the same field as `slugField`, which only builds the address.
|
|
49
|
+
- `thumbnail` (optional): a top-level `image` shown as the entry's thumbnail in the portal's list.
|
|
50
|
+
A collection with a `rich` body takes its first body image instead.
|
|
51
|
+
- `rich: true` on a `markdown` field: the portal's visual editor. Only on the `contentField`.
|
|
52
|
+
- `schedule.draft`: a top-level `boolean`. Exactly `true` makes the entry a `draft`, whatever its
|
|
53
|
+
dates: never on the live site, shown in the portal's private preview. May be declared alone.
|
|
54
|
+
|
|
55
|
+
Every site filters through `getPublished(entries, decl, now, portalPreview(process.env))` from
|
|
56
|
+
`@pmeweb/content/published`: live entries always, scheduled ones and drafts in the preview,
|
|
57
|
+
removed ones only when asked for. The `collection` helpers check the keys above, one function per
|
|
58
|
+
key so the portal can drop the faulty one alone. `prose.css`: import it in the site's
|
|
59
|
+
`Prose.astro`; it holds the structure of a row of images, the look stays per site.
|
|
60
|
+
|
|
61
|
+
Why it is major: `scheduleState` can return `draft`, and `zodFrom` refuses a collection without
|
|
62
|
+
`titleField` (or with any other error `collectionErrors` reports).
|
|
63
|
+
|
|
40
64
|
## Versioning
|
|
41
65
|
|
|
42
66
|
Sites and the portal pin an EXACT version. What `scheduleState` returns and what the field checks
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type FieldLike } from './schedule.js';
|
|
2
|
+
export interface CollectionLike {
|
|
3
|
+
titleField?: unknown;
|
|
4
|
+
thumbnail?: unknown;
|
|
5
|
+
contentField?: string;
|
|
6
|
+
fields: Record<string, FieldLike & {
|
|
7
|
+
rich?: unknown;
|
|
8
|
+
}>;
|
|
9
|
+
schedule?: {
|
|
10
|
+
publishAt?: unknown;
|
|
11
|
+
removeAt?: unknown;
|
|
12
|
+
draft?: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export declare function titleFieldError(c: CollectionLike): string | null;
|
|
16
|
+
export declare function thumbnailError(c: CollectionLike): string | null;
|
|
17
|
+
/** `rich: true` only on the `contentField`, and only on a `markdown` one. `false` means nothing. */
|
|
18
|
+
export declare function richErrors(c: CollectionLike): string[];
|
|
19
|
+
/** Every error of one collection; `[]` means valid. */
|
|
20
|
+
export declare function collectionErrors(c: CollectionLike): string[];
|
|
21
|
+
/** Every error of a whole `content.schema.json`, each prefixed with its collection's key. */
|
|
22
|
+
export declare function schemaErrors(schema: Record<string, CollectionLike>): string[];
|
|
23
|
+
/** The field that gets the visual editor, or null. */
|
|
24
|
+
export declare function richBodyField(c: CollectionLike): string | null;
|
|
25
|
+
/** Whether the portal's list draws thumbnails for this collection at all. */
|
|
26
|
+
export declare function hasThumbnails(c: CollectionLike): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @pmeweb/content/collection
|
|
2
|
+
// The collection-level keys `schedule.ts` does not own: which field is the title, which is the
|
|
3
|
+
// thumbnail, which body is rich. Checked here once, read the same way by a site's build
|
|
4
|
+
// (`zodFrom` throws on any error) and by the portal (which DROPS the faulty key with a warning,
|
|
5
|
+
// key by key -- hence one function per key, not just the total).
|
|
6
|
+
//
|
|
7
|
+
// STRUCTURAL AND LOOSE, like `SchedulableLike`: a site passes its imported JSON (every `kind`
|
|
8
|
+
// widened to `string`) and the portal its own parsed schema; neither should need a cast.
|
|
9
|
+
import { scheduleErrors } from './schedule.js';
|
|
10
|
+
export function titleFieldError(c) {
|
|
11
|
+
const name = c.titleField;
|
|
12
|
+
if (name === undefined)
|
|
13
|
+
return 'titleField : absent (le champ qui sert de titre)';
|
|
14
|
+
if (typeof name !== 'string' || !c.fields[name])
|
|
15
|
+
return `titleField : « ${String(name)} » n’est pas un champ`;
|
|
16
|
+
if (c.fields[name].kind !== 'line')
|
|
17
|
+
return `titleField : « ${name} » n’est pas une ligne de texte`;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function thumbnailError(c) {
|
|
21
|
+
const name = c.thumbnail;
|
|
22
|
+
if (name === undefined)
|
|
23
|
+
return null;
|
|
24
|
+
if (typeof name !== 'string' || !c.fields[name])
|
|
25
|
+
return `thumbnail : « ${String(name)} » n’est pas un champ`;
|
|
26
|
+
if (c.fields[name].kind !== 'image')
|
|
27
|
+
return `thumbnail : « ${name} » n’est pas une image`;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** `rich: true` only on the `contentField`, and only on a `markdown` one. `false` means nothing. */
|
|
31
|
+
export function richErrors(c) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const [key, f] of Object.entries(c.fields)) {
|
|
34
|
+
if (f.rich === undefined || f.rich === false)
|
|
35
|
+
continue;
|
|
36
|
+
if (f.rich !== true)
|
|
37
|
+
out.push(`fields.${key}.rich : seule la valeur true est permise`);
|
|
38
|
+
else if (f.kind !== 'markdown')
|
|
39
|
+
out.push(`fields.${key}.rich : « ${key} » n’est pas un champ markdown`);
|
|
40
|
+
else if (c.contentField !== key)
|
|
41
|
+
out.push(`fields.${key}.rich : « ${key} » n’est pas le contentField`);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** Every error of one collection; `[]` means valid. */
|
|
46
|
+
export function collectionErrors(c) {
|
|
47
|
+
const out = [];
|
|
48
|
+
const title = titleFieldError(c);
|
|
49
|
+
if (title)
|
|
50
|
+
out.push(title);
|
|
51
|
+
const thumb = thumbnailError(c);
|
|
52
|
+
if (thumb)
|
|
53
|
+
out.push(thumb);
|
|
54
|
+
out.push(...richErrors(c), ...scheduleErrors(c));
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/** Every error of a whole `content.schema.json`, each prefixed with its collection's key. */
|
|
58
|
+
export function schemaErrors(schema) {
|
|
59
|
+
return Object.entries(schema).flatMap(([key, c]) => collectionErrors(c).map((e) => `${key}.${e}`));
|
|
60
|
+
}
|
|
61
|
+
/** The field that gets the visual editor, or null. */
|
|
62
|
+
export function richBodyField(c) {
|
|
63
|
+
const key = c.contentField;
|
|
64
|
+
if (!key)
|
|
65
|
+
return null;
|
|
66
|
+
const f = c.fields[key];
|
|
67
|
+
return f && f.kind === 'markdown' && f.rich === true ? key : null;
|
|
68
|
+
}
|
|
69
|
+
/** Whether the portal's list draws thumbnails for this collection at all. */
|
|
70
|
+
export function hasThumbnails(c) {
|
|
71
|
+
if (typeof c.thumbnail === 'string' && c.fields[c.thumbnail]?.kind === 'image')
|
|
72
|
+
return true;
|
|
73
|
+
return richBodyField(c) !== null;
|
|
74
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @pmeweb/content/published
|
|
2
|
+
// THE ONE FILTER every site's pages go through: an entry is built when its declared schedule
|
|
3
|
+
// says it is on the site. It replaces the hand-written `isVisible` the blog sites carried and the
|
|
4
|
+
// inline `!draft` checks elsewhere -- one function, the same one the portal badges with.
|
|
5
|
+
//
|
|
6
|
+
// NO ASTRO IMPORT. The site passes `getCollection(...)`'s result, so `astro` stays an optional
|
|
7
|
+
// peer and this file stays testable.
|
|
8
|
+
import { scheduleState, visibleOnSite } from './schedule.js';
|
|
9
|
+
// `data: object`, NOT `Record<string, unknown>`: every site types its entries with an
|
|
10
|
+
// `interface` (`BlogData`, `ArticleData`), and an interface has no implicit index signature, so
|
|
11
|
+
// the narrower bound would refuse exactly the call this function exists for.
|
|
12
|
+
export function getPublished(entries, decl, now, preview) {
|
|
13
|
+
return entries.filter((e) => visibleOnSite(scheduleState(e.data, decl, now), preview));
|
|
14
|
+
}
|
package/dist/schedule.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The zone every PmeWeb client lives in. One constant, named where it is used. */
|
|
2
2
|
export declare const QUEBEC = "America/Toronto";
|
|
3
|
-
export type ScheduleState = 'scheduled' | 'live' | 'removed';
|
|
3
|
+
export type ScheduleState = 'draft' | 'scheduled' | 'live' | 'removed';
|
|
4
4
|
/**
|
|
5
5
|
* The slice of a declaration this module reads. STRUCTURAL AND LOOSE on purpose: a site passes
|
|
6
6
|
* its imported `content.schema.json` (whose `kind`s TypeScript widens to `string`) and the portal
|
|
@@ -15,6 +15,7 @@ export interface SchedulableLike {
|
|
|
15
15
|
schedule?: {
|
|
16
16
|
publishAt?: unknown;
|
|
17
17
|
removeAt?: unknown;
|
|
18
|
+
draft?: unknown;
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
@@ -41,9 +42,12 @@ export declare function scheduleErrors(c: SchedulableLike): string[];
|
|
|
41
42
|
/**
|
|
42
43
|
* Where an entry stands at `now`.
|
|
43
44
|
*
|
|
44
|
-
* `
|
|
45
|
-
*
|
|
46
|
-
*
|
|
45
|
+
* `draft` BEATS EVERYTHING: an unfinished entry is unfinished whatever its dates say, and only
|
|
46
|
+
* the exact value `true` makes one (a string « true » is a typo, and a typo must not hide a
|
|
47
|
+
* post). `scheduled` BEATS `removed`. An entry whose publication is after its removal was
|
|
48
|
+
* mis-entered, and « Programmé » is the state that makes someone look at it; « Retiré » would
|
|
49
|
+
* bury it behind the portal's filter. An invalid declaration answers `live`: it must never hide
|
|
50
|
+
* anything.
|
|
47
51
|
*/
|
|
48
52
|
export declare function scheduleState(data: Record<string, unknown>, c: SchedulableLike, now: Date): ScheduleState;
|
|
49
53
|
/**
|
|
@@ -64,5 +68,6 @@ export interface PortalPreview {
|
|
|
64
68
|
showPast: boolean;
|
|
65
69
|
}
|
|
66
70
|
export declare function portalPreview(env: Record<string, string | undefined>): PortalPreview;
|
|
67
|
-
/** What a site builds: live always; scheduled in the preview; removed only
|
|
71
|
+
/** What a site builds: live always; a draft or a scheduled entry in the preview; removed only
|
|
72
|
+
* when asked for. */
|
|
68
73
|
export declare function visibleOnSite(state: ScheduleState, preview: PortalPreview): boolean;
|
package/dist/schedule.js
CHANGED
|
@@ -111,6 +111,18 @@ export function scheduleErrors(c) {
|
|
|
111
111
|
check('publishAt', s.publishAt, false);
|
|
112
112
|
for (const p of paths(s.removeAt))
|
|
113
113
|
check('removeAt', p, true);
|
|
114
|
+
if (s.draft !== undefined) {
|
|
115
|
+
const name = s.draft;
|
|
116
|
+
if (typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
117
|
+
out.push(`schedule.draft : « ${String(name)} » n’est pas un nom de champ`);
|
|
118
|
+
}
|
|
119
|
+
else if (!c.fields[name]) {
|
|
120
|
+
out.push(`schedule.draft : « ${name} » n’est pas un champ`);
|
|
121
|
+
}
|
|
122
|
+
else if (c.fields[name].kind !== 'boolean') {
|
|
123
|
+
out.push(`schedule.draft : « ${name} » n’est pas une case à cocher`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
114
126
|
return out;
|
|
115
127
|
}
|
|
116
128
|
/** The day `removeAt` resolves to: the first path that yields one; an array path yields its latest. */
|
|
@@ -162,13 +174,19 @@ function isScheduled(data, c, now) {
|
|
|
162
174
|
/**
|
|
163
175
|
* Where an entry stands at `now`.
|
|
164
176
|
*
|
|
165
|
-
* `
|
|
166
|
-
*
|
|
167
|
-
*
|
|
177
|
+
* `draft` BEATS EVERYTHING: an unfinished entry is unfinished whatever its dates say, and only
|
|
178
|
+
* the exact value `true` makes one (a string « true » is a typo, and a typo must not hide a
|
|
179
|
+
* post). `scheduled` BEATS `removed`. An entry whose publication is after its removal was
|
|
180
|
+
* mis-entered, and « Programmé » is the state that makes someone look at it; « Retiré » would
|
|
181
|
+
* bury it behind the portal's filter. An invalid declaration answers `live`: it must never hide
|
|
182
|
+
* anything.
|
|
168
183
|
*/
|
|
169
184
|
export function scheduleState(data, c, now) {
|
|
170
185
|
if (!c.schedule || scheduleErrors(c).length > 0)
|
|
171
186
|
return 'live';
|
|
187
|
+
const draftKey = c.schedule.draft;
|
|
188
|
+
if (typeof draftKey === 'string' && data[draftKey] === true)
|
|
189
|
+
return 'draft';
|
|
172
190
|
if (isScheduled(data, c, now))
|
|
173
191
|
return 'scheduled';
|
|
174
192
|
const d = removeDay(data, c);
|
|
@@ -178,11 +196,12 @@ export function portalPreview(env) {
|
|
|
178
196
|
const active = env.PMEWEB_PORTAL_PREVIEW === '1';
|
|
179
197
|
return { active, showPast: active && env.PMEWEB_PORTAL_PREVIEW_PAST === '1' };
|
|
180
198
|
}
|
|
181
|
-
/** What a site builds: live always; scheduled in the preview; removed only
|
|
199
|
+
/** What a site builds: live always; a draft or a scheduled entry in the preview; removed only
|
|
200
|
+
* when asked for. */
|
|
182
201
|
export function visibleOnSite(state, preview) {
|
|
183
202
|
if (state === 'live')
|
|
184
203
|
return true;
|
|
185
204
|
if (!preview.active)
|
|
186
205
|
return false;
|
|
187
|
-
return state === 'scheduled' || preview.showPast;
|
|
206
|
+
return state === 'scheduled' || state === 'draft' || preview.showPast;
|
|
188
207
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Every field kind, and the whole vocabulary a site may use. Widening this is a spec
|
|
2
|
-
* decision, not a convenience: see § 4 of the design.
|
|
3
|
-
|
|
2
|
+
* decision, not a convenience: see § 4 of the design. A RUNTIME LIST so the portal's
|
|
3
|
+
* defensive parser checks against this one instead of a copy. */
|
|
4
|
+
export declare const FIELD_KINDS: readonly ["line", "paragraph", "markdown", "slug", "image", "date", "datetime", "select", "multiselect", "number", "boolean", "array", "reference", "url", "time"];
|
|
5
|
+
export type FieldKind = (typeof FIELD_KINDS)[number];
|
|
4
6
|
/** Where an enum's vocabulary lives. A POINTER at a data file, never a copy -- the portal's
|
|
5
7
|
* blog pipeline reads AND WRITES that file (design § 4). */
|
|
6
8
|
export interface OptionsRef {
|
|
@@ -46,7 +48,13 @@ export interface FieldCommon {
|
|
|
46
48
|
* for free and a missing case is a compile error.
|
|
47
49
|
*/
|
|
48
50
|
export type Field = (FieldCommon & {
|
|
49
|
-
kind: 'line' | 'paragraph' | '
|
|
51
|
+
kind: 'line' | 'paragraph' | 'slug' | 'url' | 'time' | 'date' | 'datetime' | 'boolean' | 'image';
|
|
52
|
+
})
|
|
53
|
+
/** `rich: true` gives the body the portal's visual editor. Valid ONLY on the collection's
|
|
54
|
+
* `contentField` (`richErrors`, collection.ts). Replaces the portal's hardcoded `blog`. */
|
|
55
|
+
| (FieldCommon & {
|
|
56
|
+
kind: 'markdown';
|
|
57
|
+
rich?: boolean;
|
|
50
58
|
}) | (FieldCommon & {
|
|
51
59
|
kind: 'number';
|
|
52
60
|
int?: boolean;
|
|
@@ -78,6 +86,12 @@ export interface ScheduleDecl {
|
|
|
78
86
|
* latest filled value. Read as a Quebec calendar day: visible through that whole day.
|
|
79
87
|
*/
|
|
80
88
|
removeAt?: string | string[];
|
|
89
|
+
/**
|
|
90
|
+
* One top-level `boolean` field. When it holds exactly `true` the entry is a `draft`, whatever
|
|
91
|
+
* its dates: never on the live site, shown in the portal's private preview. Absent or `false`
|
|
92
|
+
* is not a draft (booleans stay false-safe). May be declared alone.
|
|
93
|
+
*/
|
|
94
|
+
draft?: string;
|
|
81
95
|
}
|
|
82
96
|
export interface CollectionDecl {
|
|
83
97
|
label: string;
|
|
@@ -87,6 +101,12 @@ export interface CollectionDecl {
|
|
|
87
101
|
entryFile: string;
|
|
88
102
|
/** The field the slug is derived from AT CREATION. Never re-derived on edit (design § 6). */
|
|
89
103
|
slugField: string;
|
|
104
|
+
/** The entry's human title: lists, reference dropdowns, confirmations. A top-level `line`.
|
|
105
|
+
* REQUIRED. May name the same field as `slugField`, which only builds the address. */
|
|
106
|
+
titleField: string;
|
|
107
|
+
/** A top-level `image` field shown as the entry's thumbnail in the portal's list. Optional:
|
|
108
|
+
* a collection with a `rich` body takes its first body image instead (`hasThumbnails`). */
|
|
109
|
+
thumbnail?: string;
|
|
90
110
|
/** Which field is the Markdown body. ABSENT on a collection that has no body -- `authors`
|
|
91
111
|
* is frontmatter only. Excluded from the generated zod: Astro's body is not a field. */
|
|
92
112
|
contentField?: string;
|
package/dist/types.js
CHANGED
|
@@ -5,4 +5,10 @@
|
|
|
5
5
|
// WHY THE JSON IS TYPED AT ALL. A mistyped `kind` is otherwise a runtime throw during
|
|
6
6
|
// `astro build`, in a file with no line number that a reader associates with content. With
|
|
7
7
|
// this, `npm run check` names the field.
|
|
8
|
-
|
|
8
|
+
/** Every field kind, and the whole vocabulary a site may use. Widening this is a spec
|
|
9
|
+
* decision, not a convenience: see § 4 of the design. A RUNTIME LIST so the portal's
|
|
10
|
+
* defensive parser checks against this one instead of a copy. */
|
|
11
|
+
export const FIELD_KINDS = [
|
|
12
|
+
'line', 'paragraph', 'markdown', 'slug', 'image', 'date', 'datetime',
|
|
13
|
+
'select', 'multiselect', 'number', 'boolean', 'array', 'reference', 'url', 'time',
|
|
14
|
+
];
|
package/dist/zodFrom.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// reason, which is why the generator lives here and takes what it needs as arguments.
|
|
20
20
|
import { z } from 'astro/zod';
|
|
21
21
|
import { TIME_RE, isDate, isDateTime, isDayDate } from './fields.js';
|
|
22
|
-
import {
|
|
22
|
+
import { collectionErrors } from './collection.js';
|
|
23
23
|
function values(from, ctx) {
|
|
24
24
|
const vocab = ctx.vocabularies[from];
|
|
25
25
|
// THROWS rather than falling back to z.string(). A silent fallback accepts any value, and
|
|
@@ -139,9 +139,10 @@ function objectSchema(fields, ctx) {
|
|
|
139
139
|
* to the type is silently unreachable in every template).
|
|
140
140
|
*/
|
|
141
141
|
export function zodFrom(decl, ctx) {
|
|
142
|
-
// A
|
|
143
|
-
//
|
|
144
|
-
|
|
142
|
+
// A declaration the package refuses -- a title that is not a line, a rich flag off the body, a
|
|
143
|
+
// schedule pointing at a non-date -- fails the build that introduced it. The portal drops the
|
|
144
|
+
// same key with a warning instead (`collection.ts`, `schedule.ts`).
|
|
145
|
+
const errors = collectionErrors(decl);
|
|
145
146
|
if (errors.length > 0)
|
|
146
147
|
throw new Error(`zodFrom: ${errors.join(' ; ')}`);
|
|
147
148
|
const fields = Object.fromEntries(Object.entries(decl.fields).filter(([key]) => key !== decl.contentField));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/content",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "PmeWeb content contract: content.schema.json types, zodFrom, field checks, scheduling. Read by every site's build and by the client portal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"prose.css"
|
|
12
13
|
],
|
|
13
14
|
"exports": {
|
|
14
15
|
"./types": {
|
|
@@ -26,7 +27,16 @@
|
|
|
26
27
|
"./zodFrom": {
|
|
27
28
|
"types": "./dist/zodFrom.d.ts",
|
|
28
29
|
"default": "./dist/zodFrom.js"
|
|
29
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"./collection": {
|
|
32
|
+
"types": "./dist/collection.d.ts",
|
|
33
|
+
"default": "./dist/collection.js"
|
|
34
|
+
},
|
|
35
|
+
"./published": {
|
|
36
|
+
"types": "./dist/published.d.ts",
|
|
37
|
+
"default": "./dist/published.js"
|
|
38
|
+
},
|
|
39
|
+
"./prose.css": "./prose.css"
|
|
30
40
|
},
|
|
31
41
|
"scripts": {
|
|
32
42
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|
package/prose.css
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* @pmeweb/content/prose.css -- the STRUCTURE a row of images needs, and nothing else.
|
|
2
|
+
* A paragraph holding two or more images (consecutive lines, no blank line) is a row; a blank
|
|
3
|
+
* line keeps them stacked. The portal's editor writes rows (chunk 9), so every site whose body is
|
|
4
|
+
* `rich` must show one: a copied rule is one a site forgets, and the client's click then does
|
|
5
|
+
* nothing with no error anywhere. The LOOK (border, radius, the row's margin) stays per site.
|
|
6
|
+
* Global on purpose: import it from the site's Prose component. */
|
|
7
|
+
.prose p:has(img + img) {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-wrap: wrap;
|
|
10
|
+
align-items: flex-start;
|
|
11
|
+
gap: var(--prose-row-gap, 14px);
|
|
12
|
+
}
|
|
13
|
+
.prose p:has(img + img) img {
|
|
14
|
+
flex: 1 1 260px;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|