koguma 0.4.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/LICENSE +21 -0
- package/README.md +297 -0
- package/cli/index.ts +1150 -0
- package/package.json +60 -0
- package/src/admin/_bundle.ts +3 -0
- package/src/admin/dashboard.ts +27 -0
- package/src/api/router.ts +357 -0
- package/src/auth/index.ts +138 -0
- package/src/client/index.ts +61 -0
- package/src/config/define.ts +157 -0
- package/src/config/field.ts +182 -0
- package/src/config/index.ts +27 -0
- package/src/config/meta.ts +189 -0
- package/src/config/types.ts +35 -0
- package/src/db/migrate.ts +146 -0
- package/src/db/queries.ts +293 -0
- package/src/db/schema.ts +115 -0
- package/src/media/index.ts +89 -0
- package/src/react/index.ts +70 -0
- package/src/worker.ts +51 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contentType, group, defineConfig, Infer — the schema definition API.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from 'zod/v4';
|
|
5
|
+
import type { FieldBuilder, FieldMeta } from './field.ts';
|
|
6
|
+
|
|
7
|
+
// ── Group ────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface GroupConfig {
|
|
10
|
+
groupId: string;
|
|
11
|
+
name: string;
|
|
12
|
+
helpText?: string;
|
|
13
|
+
collapsed?: boolean;
|
|
14
|
+
fields: Record<string, FieldBuilder>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function group(opts: {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
helpText?: string;
|
|
21
|
+
collapsed?: boolean;
|
|
22
|
+
fields: Record<string, FieldBuilder>;
|
|
23
|
+
}): GroupConfig {
|
|
24
|
+
return {
|
|
25
|
+
groupId: opts.id,
|
|
26
|
+
name: opts.name,
|
|
27
|
+
helpText: opts.helpText,
|
|
28
|
+
collapsed: opts.collapsed,
|
|
29
|
+
fields: opts.fields
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── ContentType ──────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface ContentTypeConfig<S extends z.ZodType = z.ZodType> {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
displayField: string;
|
|
39
|
+
singleton?: boolean;
|
|
40
|
+
/** The resolved Zod schema for z.infer */
|
|
41
|
+
schema: S;
|
|
42
|
+
/** Field metadata for the admin + schema generator */
|
|
43
|
+
fieldMeta: Record<string, FieldMeta>;
|
|
44
|
+
/** Group layout for the admin */
|
|
45
|
+
groups: GroupConfig[];
|
|
46
|
+
/** Flat fields (no groups) — for simple content types */
|
|
47
|
+
flatFields: Record<string, FieldBuilder>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Define a content type.
|
|
52
|
+
*
|
|
53
|
+
* Fields can be defined directly (flat) or organized into groups:
|
|
54
|
+
*
|
|
55
|
+
* ```ts
|
|
56
|
+
* // Flat
|
|
57
|
+
* contentType("card", {
|
|
58
|
+
* name: "Card",
|
|
59
|
+
* displayField: "title",
|
|
60
|
+
* fields: {
|
|
61
|
+
* title: field.text("Title").required(),
|
|
62
|
+
* body: field.richText("Body"),
|
|
63
|
+
* },
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* // Grouped
|
|
67
|
+
* contentType("page", {
|
|
68
|
+
* name: "Page",
|
|
69
|
+
* displayField: "title",
|
|
70
|
+
* fields: [
|
|
71
|
+
* group({ id: "hero", name: "1 · Hero", fields: { title: field.text("Title") } }),
|
|
72
|
+
* group({ id: "about", name: "2 · About", fields: { heading: field.text("Heading") } }),
|
|
73
|
+
* ],
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function contentType<
|
|
78
|
+
F extends Record<string, FieldBuilder> | GroupConfig[]
|
|
79
|
+
>(opts: {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
displayField: string;
|
|
83
|
+
singleton?: boolean;
|
|
84
|
+
fields: F;
|
|
85
|
+
}): ContentTypeConfig {
|
|
86
|
+
// Collect all field builders (from groups or flat)
|
|
87
|
+
const allFields: Record<string, FieldBuilder> = {};
|
|
88
|
+
const groups: GroupConfig[] = [];
|
|
89
|
+
let flatFields: Record<string, FieldBuilder> = {};
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(opts.fields)) {
|
|
92
|
+
// Grouped fields
|
|
93
|
+
for (const g of opts.fields) {
|
|
94
|
+
groups.push(g);
|
|
95
|
+
Object.assign(allFields, g.fields);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Flat fields
|
|
99
|
+
flatFields = opts.fields as Record<string, FieldBuilder>;
|
|
100
|
+
Object.assign(allFields, flatFields);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build Zod object schema from all fields
|
|
104
|
+
const shape: Record<string, z.ZodType> = {};
|
|
105
|
+
const fieldMeta: Record<string, FieldMeta> = {};
|
|
106
|
+
|
|
107
|
+
for (const [key, builder] of Object.entries(allFields)) {
|
|
108
|
+
shape[key] = builder._schema;
|
|
109
|
+
fieldMeta[key] = builder._meta;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const schema = z.object(shape);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
id: opts.id,
|
|
116
|
+
name: opts.name,
|
|
117
|
+
displayField: opts.displayField,
|
|
118
|
+
singleton: opts.singleton,
|
|
119
|
+
schema: schema as z.ZodType,
|
|
120
|
+
fieldMeta,
|
|
121
|
+
groups,
|
|
122
|
+
flatFields
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── defineConfig ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
export interface KogumaConfig {
|
|
129
|
+
siteName: string;
|
|
130
|
+
contentTypes: ContentTypeConfig[];
|
|
131
|
+
hooks?: {
|
|
132
|
+
beforeSave?: (
|
|
133
|
+
entry: Record<string, unknown>,
|
|
134
|
+
ctx: { contentType: string }
|
|
135
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
136
|
+
afterSave?: (
|
|
137
|
+
entry: Record<string, unknown>,
|
|
138
|
+
ctx: { contentType: string }
|
|
139
|
+
) => void | Promise<void>;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function defineConfig(config: KogumaConfig): KogumaConfig {
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Infer ────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract the TypeScript type from a content type definition.
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* const page = contentType({ ... });
|
|
154
|
+
* type Page = Infer<typeof page>;
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export type Infer<T extends ContentTypeConfig> = z.infer<T['schema']>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* field.* builders — the user-facing API for defining content fields.
|
|
3
|
+
*
|
|
4
|
+
* Each method returns a FieldBuilder that wraps a Zod schema internally.
|
|
5
|
+
* The admin label is always the first argument for readability:
|
|
6
|
+
*
|
|
7
|
+
* field.text("Page Title").required().max(100)
|
|
8
|
+
* field.richText("Body Text")
|
|
9
|
+
* field.image("Hero Photo")
|
|
10
|
+
* field.refs("featureCard", "Highlights")
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod/v4";
|
|
13
|
+
import type { KogumaAsset, RichTextDocument, EntryReference } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
// ── Field metadata (extracted by the admin + schema generator) ──────
|
|
16
|
+
|
|
17
|
+
export type FieldType =
|
|
18
|
+
| "text"
|
|
19
|
+
| "longText"
|
|
20
|
+
| "richText"
|
|
21
|
+
| "url"
|
|
22
|
+
| "image"
|
|
23
|
+
| "boolean"
|
|
24
|
+
| "number"
|
|
25
|
+
| "date"
|
|
26
|
+
| "select"
|
|
27
|
+
| "reference"
|
|
28
|
+
| "references";
|
|
29
|
+
|
|
30
|
+
export interface FieldMeta {
|
|
31
|
+
label: string;
|
|
32
|
+
fieldType: FieldType;
|
|
33
|
+
required: boolean;
|
|
34
|
+
refContentType?: string;
|
|
35
|
+
options?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── FieldBuilder ────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
export class FieldBuilder<T extends z.ZodType = z.ZodType> {
|
|
41
|
+
/** @internal */
|
|
42
|
+
readonly _schema: T;
|
|
43
|
+
/** @internal */
|
|
44
|
+
readonly _meta: FieldMeta;
|
|
45
|
+
|
|
46
|
+
constructor(schema: T, meta: FieldMeta) {
|
|
47
|
+
this._schema = schema;
|
|
48
|
+
this._meta = meta;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mark as required (fields are optional by default) */
|
|
52
|
+
required(): FieldBuilder<T> {
|
|
53
|
+
return new FieldBuilder(this._schema, { ...this._meta, required: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Set maximum length (text) or value (number) */
|
|
57
|
+
max(value: number): FieldBuilder<T> {
|
|
58
|
+
const s = this._schema;
|
|
59
|
+
if (s instanceof z.ZodString) {
|
|
60
|
+
return new FieldBuilder(s.max(value) as unknown as T, this._meta);
|
|
61
|
+
}
|
|
62
|
+
if (s instanceof z.ZodNumber) {
|
|
63
|
+
return new FieldBuilder(s.max(value) as unknown as T, this._meta);
|
|
64
|
+
}
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Set minimum length (text) or value (number) */
|
|
69
|
+
min(value: number): FieldBuilder<T> {
|
|
70
|
+
const s = this._schema;
|
|
71
|
+
if (s instanceof z.ZodString) {
|
|
72
|
+
return new FieldBuilder(s.min(value) as unknown as T, this._meta);
|
|
73
|
+
}
|
|
74
|
+
if (s instanceof z.ZodNumber) {
|
|
75
|
+
return new FieldBuilder(s.min(value) as unknown as T, this._meta);
|
|
76
|
+
}
|
|
77
|
+
if (s instanceof z.ZodArray) {
|
|
78
|
+
return new FieldBuilder(s.min(value) as unknown as T, this._meta);
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Set a default value */
|
|
84
|
+
default(value: z.infer<T>): FieldBuilder {
|
|
85
|
+
return new FieldBuilder((this._schema as z.ZodType).default(value as never), this._meta);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Factory functions ──────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export const field = {
|
|
92
|
+
/** Short text field */
|
|
93
|
+
text(label: string) {
|
|
94
|
+
return new FieldBuilder(
|
|
95
|
+
z.string().optional().describe(label),
|
|
96
|
+
{ label, fieldType: "text", required: false },
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/** Multi-line text field */
|
|
101
|
+
longText(label: string) {
|
|
102
|
+
return new FieldBuilder(
|
|
103
|
+
z.string().optional().describe(label),
|
|
104
|
+
{ label, fieldType: "longText", required: false },
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** Rich text field (Tiptap JSON document) */
|
|
109
|
+
richText(label: string) {
|
|
110
|
+
return new FieldBuilder(
|
|
111
|
+
z.custom<RichTextDocument>().optional().describe(label),
|
|
112
|
+
{ label, fieldType: "richText", required: false },
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/** URL field with validation */
|
|
117
|
+
url(label: string) {
|
|
118
|
+
return new FieldBuilder(
|
|
119
|
+
z.string().url().optional().describe(label),
|
|
120
|
+
{ label, fieldType: "url", required: false },
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/** Image/media field (reference to R2 asset) */
|
|
125
|
+
image(label: string) {
|
|
126
|
+
return new FieldBuilder(
|
|
127
|
+
z.custom<KogumaAsset>().optional().describe(label),
|
|
128
|
+
{ label, fieldType: "image", required: false },
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/** Boolean toggle */
|
|
133
|
+
boolean(label: string) {
|
|
134
|
+
return new FieldBuilder(
|
|
135
|
+
z.boolean().optional().describe(label),
|
|
136
|
+
{ label, fieldType: "boolean", required: false },
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/** Numeric field */
|
|
141
|
+
number(label: string) {
|
|
142
|
+
return new FieldBuilder(
|
|
143
|
+
z.number().optional().describe(label),
|
|
144
|
+
{ label, fieldType: "number", required: false },
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** Date field */
|
|
149
|
+
date(label: string) {
|
|
150
|
+
return new FieldBuilder(
|
|
151
|
+
z.string().datetime().optional().describe(label),
|
|
152
|
+
{ label, fieldType: "date", required: false },
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/** Dropdown select */
|
|
157
|
+
select(label: string, opts: { options: string[] }) {
|
|
158
|
+
const enumSchema = z.enum(opts.options as [string, ...string[]]).optional().describe(label);
|
|
159
|
+
return new FieldBuilder(enumSchema, {
|
|
160
|
+
label,
|
|
161
|
+
fieldType: "select",
|
|
162
|
+
required: false,
|
|
163
|
+
options: opts.options,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/** Single reference to another content type */
|
|
168
|
+
ref(contentType: string, label: string) {
|
|
169
|
+
return new FieldBuilder(
|
|
170
|
+
z.custom<EntryReference>().optional().describe(label),
|
|
171
|
+
{ label, fieldType: "reference", required: false, refContentType: contentType },
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
/** Ordered array of references to another content type */
|
|
176
|
+
refs(contentType: string, label: string) {
|
|
177
|
+
return new FieldBuilder(
|
|
178
|
+
z.array(z.custom<EntryReference>()).optional().describe(label),
|
|
179
|
+
{ label, fieldType: "references", required: false, refContentType: contentType },
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Koguma — public API
|
|
3
|
+
*
|
|
4
|
+
* Everything the user needs comes from this single import:
|
|
5
|
+
*
|
|
6
|
+
* import { defineConfig, contentType, group, field, type Infer } from "koguma";
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Config helpers
|
|
10
|
+
export { defineConfig, contentType, group } from './define.ts';
|
|
11
|
+
export type {
|
|
12
|
+
Infer,
|
|
13
|
+
KogumaConfig,
|
|
14
|
+
ContentTypeConfig,
|
|
15
|
+
GroupConfig
|
|
16
|
+
} from './define.ts';
|
|
17
|
+
|
|
18
|
+
// Field builders
|
|
19
|
+
export { field } from './field.ts';
|
|
20
|
+
export type { FieldBuilder, FieldType, FieldMeta } from './field.ts';
|
|
21
|
+
|
|
22
|
+
// Runtime types
|
|
23
|
+
export type { KogumaAsset, RichTextDocument, EntryReference } from './types.ts';
|
|
24
|
+
|
|
25
|
+
// Meta registry (for dev tools like the Schema Builder)
|
|
26
|
+
export { fieldRegistry, fieldSuggestions } from './meta.ts';
|
|
27
|
+
export type { FieldTypeMeta, FieldSuggestion } from './meta.ts';
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* koguma/meta — field type registry for tooling.
|
|
3
|
+
*
|
|
4
|
+
* This module exports metadata about every field type Koguma supports.
|
|
5
|
+
* The Schema Builder (and any future dev tools) reads this registry
|
|
6
|
+
* instead of hardcoding field type lists.
|
|
7
|
+
*
|
|
8
|
+
* Zero breaking changes — purely additive. Infer<T> and all existing
|
|
9
|
+
* APIs are untouched.
|
|
10
|
+
*/
|
|
11
|
+
import type { FieldType } from './field.ts';
|
|
12
|
+
|
|
13
|
+
// ── Field Type Metadata ─────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface FieldTypeMeta {
|
|
16
|
+
type: FieldType;
|
|
17
|
+
label: string;
|
|
18
|
+
icon: string;
|
|
19
|
+
description: string;
|
|
20
|
+
category: 'basics' | 'data' | 'relations';
|
|
21
|
+
args: 'label' | 'label+options' | 'ref+label';
|
|
22
|
+
modifiers: ('required' | 'min' | 'max' | 'default')[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const fieldRegistry: FieldTypeMeta[] = [
|
|
26
|
+
// Basics
|
|
27
|
+
{
|
|
28
|
+
type: 'text',
|
|
29
|
+
label: 'Text',
|
|
30
|
+
icon: 'Aa',
|
|
31
|
+
category: 'basics',
|
|
32
|
+
description: 'Short text',
|
|
33
|
+
args: 'label',
|
|
34
|
+
modifiers: ['required', 'min', 'max', 'default']
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'longText',
|
|
38
|
+
label: 'Long Text',
|
|
39
|
+
icon: '¶',
|
|
40
|
+
category: 'basics',
|
|
41
|
+
description: 'Multi-line text',
|
|
42
|
+
args: 'label',
|
|
43
|
+
modifiers: ['required', 'min', 'max', 'default']
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'richText',
|
|
47
|
+
label: 'Rich Text',
|
|
48
|
+
icon: '📝',
|
|
49
|
+
category: 'basics',
|
|
50
|
+
description: 'Rich text editor',
|
|
51
|
+
args: 'label',
|
|
52
|
+
modifiers: ['required']
|
|
53
|
+
},
|
|
54
|
+
// Data
|
|
55
|
+
{
|
|
56
|
+
type: 'url',
|
|
57
|
+
label: 'URL',
|
|
58
|
+
icon: '🔗',
|
|
59
|
+
category: 'data',
|
|
60
|
+
description: 'URL with validation',
|
|
61
|
+
args: 'label',
|
|
62
|
+
modifiers: ['required']
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
label: 'Boolean',
|
|
67
|
+
icon: '☑',
|
|
68
|
+
category: 'data',
|
|
69
|
+
description: 'True/false toggle',
|
|
70
|
+
args: 'label',
|
|
71
|
+
modifiers: ['required', 'default']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'number',
|
|
75
|
+
label: 'Number',
|
|
76
|
+
icon: '#',
|
|
77
|
+
category: 'data',
|
|
78
|
+
description: 'Numeric value',
|
|
79
|
+
args: 'label',
|
|
80
|
+
modifiers: ['required', 'min', 'max', 'default']
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'date',
|
|
84
|
+
label: 'Date',
|
|
85
|
+
icon: '📅',
|
|
86
|
+
category: 'data',
|
|
87
|
+
description: 'Date picker',
|
|
88
|
+
args: 'label',
|
|
89
|
+
modifiers: ['required']
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'select',
|
|
93
|
+
label: 'Select',
|
|
94
|
+
icon: '▾',
|
|
95
|
+
category: 'data',
|
|
96
|
+
description: 'Dropdown options',
|
|
97
|
+
args: 'label+options',
|
|
98
|
+
modifiers: ['required', 'default']
|
|
99
|
+
},
|
|
100
|
+
// Relations
|
|
101
|
+
{
|
|
102
|
+
type: 'image',
|
|
103
|
+
label: 'Image',
|
|
104
|
+
icon: '🖼️',
|
|
105
|
+
category: 'relations',
|
|
106
|
+
description: 'Image from R2',
|
|
107
|
+
args: 'label',
|
|
108
|
+
modifiers: ['required']
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'reference',
|
|
112
|
+
label: 'Reference',
|
|
113
|
+
icon: '→',
|
|
114
|
+
category: 'relations',
|
|
115
|
+
description: 'Link to entry',
|
|
116
|
+
args: 'ref+label',
|
|
117
|
+
modifiers: ['required']
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
type: 'references',
|
|
121
|
+
label: 'References',
|
|
122
|
+
icon: '⇉',
|
|
123
|
+
category: 'relations',
|
|
124
|
+
description: 'List of links',
|
|
125
|
+
args: 'ref+label',
|
|
126
|
+
modifiers: ['required', 'min']
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// ── Intent-based field suggestions ───────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export interface FieldSuggestion {
|
|
133
|
+
type: FieldType;
|
|
134
|
+
modifiers?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Maps common label keywords to suggested field types.
|
|
139
|
+
* The builder uses this to auto-select the type dropdown when
|
|
140
|
+
* the user types a label.
|
|
141
|
+
*/
|
|
142
|
+
export const fieldSuggestions: Record<string, FieldSuggestion> = {
|
|
143
|
+
// Text fields
|
|
144
|
+
title: { type: 'text', modifiers: 'required' },
|
|
145
|
+
name: { type: 'text', modifiers: 'required' },
|
|
146
|
+
slug: { type: 'text', modifiers: 'required' },
|
|
147
|
+
heading: { type: 'text' },
|
|
148
|
+
tagline: { type: 'text' },
|
|
149
|
+
// Long text
|
|
150
|
+
description: { type: 'longText' },
|
|
151
|
+
bio: { type: 'longText' },
|
|
152
|
+
notes: { type: 'longText' },
|
|
153
|
+
excerpt: { type: 'longText' },
|
|
154
|
+
// Rich text
|
|
155
|
+
body: { type: 'richText' },
|
|
156
|
+
content: { type: 'richText' },
|
|
157
|
+
story: { type: 'richText' },
|
|
158
|
+
// URL
|
|
159
|
+
email: { type: 'url' },
|
|
160
|
+
website: { type: 'url' },
|
|
161
|
+
url: { type: 'url' },
|
|
162
|
+
link: { type: 'url' },
|
|
163
|
+
// Image
|
|
164
|
+
photo: { type: 'image' },
|
|
165
|
+
image: { type: 'image' },
|
|
166
|
+
avatar: { type: 'image' },
|
|
167
|
+
cover: { type: 'image' },
|
|
168
|
+
logo: { type: 'image' },
|
|
169
|
+
poster: { type: 'image' },
|
|
170
|
+
// Boolean
|
|
171
|
+
published: { type: 'boolean', modifiers: 'default(false)' },
|
|
172
|
+
active: { type: 'boolean', modifiers: 'default(true)' },
|
|
173
|
+
featured: { type: 'boolean', modifiers: 'default(false)' },
|
|
174
|
+
// Number
|
|
175
|
+
price: { type: 'number', modifiers: 'min(0)' },
|
|
176
|
+
weight: { type: 'number' },
|
|
177
|
+
count: { type: 'number' },
|
|
178
|
+
order: { type: 'number' },
|
|
179
|
+
// Date
|
|
180
|
+
date: { type: 'date' },
|
|
181
|
+
birthday: { type: 'date' },
|
|
182
|
+
// Select
|
|
183
|
+
category: { type: 'select' },
|
|
184
|
+
status: { type: 'select' },
|
|
185
|
+
role: { type: 'select' },
|
|
186
|
+
type: { type: 'select' },
|
|
187
|
+
genre: { type: 'select' },
|
|
188
|
+
format: { type: 'select' }
|
|
189
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core CMS types used across Koguma.
|
|
3
|
+
* These are the runtime types for assets and rich text — not Zod schemas.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** A media asset stored in R2 */
|
|
7
|
+
export interface KogumaAsset {
|
|
8
|
+
id: string;
|
|
9
|
+
url: string;
|
|
10
|
+
title: string;
|
|
11
|
+
contentType: string;
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A Tiptap/ProseMirror-compatible rich text document */
|
|
17
|
+
export interface RichTextDocument {
|
|
18
|
+
type: "doc";
|
|
19
|
+
content: RichTextNode[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RichTextNode {
|
|
23
|
+
type: string;
|
|
24
|
+
content?: RichTextNode[];
|
|
25
|
+
text?: string;
|
|
26
|
+
marks?: { type: string; attrs?: Record<string, unknown> }[];
|
|
27
|
+
attrs?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Reference to another entry */
|
|
31
|
+
export interface EntryReference<T = Record<string, unknown>> {
|
|
32
|
+
id: string;
|
|
33
|
+
contentType: string;
|
|
34
|
+
fields?: T;
|
|
35
|
+
}
|