@nonext/husk 0.2.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/CHANGELOG.md +148 -0
- package/LICENSE +166 -0
- package/README.md +140 -0
- package/api/public-api.md +1070 -0
- package/dist/adapter-C_0mhENy.d.ts +320 -0
- package/dist/admin/admin.css +5 -0
- package/dist/admin/index.d.ts +265 -0
- package/dist/admin/index.js +393 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/tokens.css +597 -0
- package/dist/admin/ui.d.ts +3013 -0
- package/dist/admin/ui.js +9783 -0
- package/dist/admin/ui.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth-B2gBtHl5.d.ts +28 -0
- package/dist/can-Chy7zmx7.d.ts +69 -0
- package/dist/chunk-3I6337WJ.js +83 -0
- package/dist/chunk-3I6337WJ.js.map +1 -0
- package/dist/chunk-4Z6P3XCE.js +62 -0
- package/dist/chunk-4Z6P3XCE.js.map +1 -0
- package/dist/chunk-6E3ZS5Z5.js +1007 -0
- package/dist/chunk-6E3ZS5Z5.js.map +1 -0
- package/dist/chunk-77JDRXXU.js +3 -0
- package/dist/chunk-77JDRXXU.js.map +1 -0
- package/dist/chunk-FHYWJCG6.js +171 -0
- package/dist/chunk-FHYWJCG6.js.map +1 -0
- package/dist/chunk-JAE7HHMM.js +506 -0
- package/dist/chunk-JAE7HHMM.js.map +1 -0
- package/dist/chunk-NPDBQBLW.js +68 -0
- package/dist/chunk-NPDBQBLW.js.map +1 -0
- package/dist/chunk-ORMYARXD.js +6 -0
- package/dist/chunk-ORMYARXD.js.map +1 -0
- package/dist/chunk-R4O7APIX.js +51 -0
- package/dist/chunk-R4O7APIX.js.map +1 -0
- package/dist/chunk-ROLKRJWI.js +187 -0
- package/dist/chunk-ROLKRJWI.js.map +1 -0
- package/dist/chunk-SOTQ3WP5.js +31 -0
- package/dist/chunk-SOTQ3WP5.js.map +1 -0
- package/dist/chunk-TCFFHTY3.js +97 -0
- package/dist/chunk-TCFFHTY3.js.map +1 -0
- package/dist/chunk-TS7Y7GEZ.js +7 -0
- package/dist/chunk-TS7Y7GEZ.js.map +1 -0
- package/dist/chunk-VLGR5PZX.js +3 -0
- package/dist/chunk-VLGR5PZX.js.map +1 -0
- package/dist/chunk-ZW2PUSVS.js +40 -0
- package/dist/chunk-ZW2PUSVS.js.map +1 -0
- package/dist/chunk-ZYNBMYJ6.js +392 -0
- package/dist/chunk-ZYNBMYJ6.js.map +1 -0
- package/dist/cli/nonext-husk.js +3360 -0
- package/dist/cli/nonext-husk.js.map +1 -0
- package/dist/cli/templates/firestore.rules +393 -0
- package/dist/cli/templates/storage.rules +165 -0
- package/dist/client/index.d.ts +27 -0
- package/dist/client/index.js +15 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cms-9fSA3uQV.d.ts +158 -0
- package/dist/core/index.d.ts +1267 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/entry-D3kiE8T1.d.ts +163 -0
- package/dist/field-Diyf5siF.d.ts +158 -0
- package/dist/field-type-D25Vojb0.d.ts +157 -0
- package/dist/firebase/index.d.ts +251 -0
- package/dist/firebase/index.js +991 -0
- package/dist/firebase/index.js.map +1 -0
- package/dist/index-5ylaMBUH.d.ts +155 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/index.d.ts +101 -0
- package/dist/permissions/index.js +7 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/registry-CUC25qay.d.ts +59 -0
- package/dist/richtext/index.d.ts +85 -0
- package/dist/richtext/index.js +127 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/schema/index.d.ts +50 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +25 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/write-limits-Djmm6L3R.d.ts +215 -0
- package/package.json +136 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { ENTRY_COLLECTION_GROUP, ERROR_CATEGORIES, FALLBACK_SORT, HUSK_TAG_PREFIX, MAX_ENTRY_INDEXES, MAX_FIELD_COLUMNS, MIGRATION_BATCH_SIZE, MIGRATION_MAX_BATCHES, STATUS_EFFECT, UNTITLED_ENTRY, VALIDATION_RULE_CONTROLS, applyPlan, buildCountQuery, buildListQuery, canSaveEntry, canSearch, checkFieldKey, checkNestedFieldKey, classifyError, confirmationMatches, contentTypeTag, createMigrationRegistry, createRegistryStore, defaultListSort, deleteConfirmationName, deleteConfirmationPhrase, deniedError, draftIssues, entryChangedSince, entrySlugTag, entryTag, entryTags, entryTitle, entryUniqueCheck, errorsUnder, fieldOptionIssues, formFields, formValuesFromEntry, indexCovered, initialFormValues, initialListState, isFilterableField, isHuskTag, isNarrowed, isSearching, listColumns, listFilterQuery, listFilters, mediaTag, mergeIssues, missingEntryIndexes, missingError, moveFieldRow, moveItem, newFieldDefinition, nextSort, normalizeConfirmation, orderedFieldKeys, orderedFields, requiredEntryIndexes, runMigration, sameDefinition, settingsTag, sortableFieldKeys, staleWriteError, statusTransitions, suggestFieldKey, treatmentFor, unroutableIssues, valuesChanged, withFieldOption, withFieldRows, withValidationRule } from '../chunk-6E3ZS5Z5.js';
|
|
2
|
+
export { createCMS } from '../chunk-3I6337WJ.js';
|
|
3
|
+
export { ALLOWED_MEDIA_TYPES, DEFAULT_PAGE_SIZE, FIRESTORE_DOCUMENT_BYTES, INITIAL_SCHEMA_VERSION, MAX_ARRAY_ITEMS, MAX_CONTENT_TYPE_FIELDS, MAX_ENTRY_FIELDS, MAX_FIELD_PURGE, MAX_IMAGE_BYTES, MAX_MEDIA_BYTES, MAX_MEDIA_REFS, MAX_NESTING_DEPTH, MAX_PAGE_SIZE, MAX_SETTINGS_FIELDS, MAX_WRITE_BYTES, MEDIA_ACCEPT_ATTRIBUTE, MEDIA_REFS_KEY, MEDIA_USAGE_LIMIT, MIN_WRITE_INTERVAL_MS, WRITE_BURST_LIMIT, WRITE_BURST_WINDOW_MS, addField, approximateBytes, assertSchemaWritable, buildRegistry, checkMediaFile, checkWritePayload, collectMediaIds, createWriteRateLimiter, fieldKeyState, fieldMapSignature, fileExtension, forgetRemovedField, isSchemaReadOnly, mediaTypeRule, nextSchemaVersion, normalizeLimit, normalizeMediaType, removeField, removedFieldEntries, restoreField, retargetMediaUrls, safeFileName, schemaVersionOf, slugFieldKey, storageShapeChanged, storageSignature, storageSignatureOf, storesTheSameWay } from '../chunk-JAE7HHMM.js';
|
|
4
|
+
export { INITIAL_FIELD_KEY, RESERVED_TYPE_SLUGS, assertTypeSlugAvailable, assertValidContentType, checkTypeSlug, createContentType, defineContentType, newContentType } from '../chunk-ROLKRJWI.js';
|
|
5
|
+
import '../chunk-SOTQ3WP5.js';
|
|
6
|
+
export { assertDeletable, assertListable, assertSingletonCreatable, assertSingletonEntryId, isCollection, isSingleton, singletonEntryId } from '../chunk-R4O7APIX.js';
|
|
7
|
+
import '../chunk-TCFFHTY3.js';
|
|
8
|
+
import '../chunk-NPDBQBLW.js';
|
|
9
|
+
import '../chunk-TS7Y7GEZ.js';
|
|
10
|
+
export { LockedTypeError, PermissionError, SchemaError, SingletonError, ValidationError, WriteLimitError } from '../chunk-4Z6P3XCE.js';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { c as FieldMap, a as FieldDefinition, f as FieldValues, b as FieldInputValues } from './field-Diyf5siF.js';
|
|
2
|
+
import { d as PermissionMatrix } from './auth-B2gBtHl5.js';
|
|
3
|
+
|
|
4
|
+
type ContentTypeKind = "collection" | "singleton";
|
|
5
|
+
/**
|
|
6
|
+
* A field that was removed from a content type while its stored values were kept.
|
|
7
|
+
*
|
|
8
|
+
* Removal is a state, not an absence. The definition is parked here instead of being
|
|
9
|
+
* deleted, so the admin can name the field in a warning, offer it back unchanged, and so a
|
|
10
|
+
* later purge knows what it is about to destroy. Nothing downstream of the registry reads
|
|
11
|
+
* this map: forms, validation, list columns and the SDK all read `fields`, which is why a
|
|
12
|
+
* removed field is inert everywhere without a single consumer having to filter it out.
|
|
13
|
+
*/
|
|
14
|
+
interface RemovedField {
|
|
15
|
+
/** Exactly the definition the field had at the moment it was removed. */
|
|
16
|
+
field: FieldDefinition;
|
|
17
|
+
/** ISO 8601. A plain string, because a stored content type is plain JSON. */
|
|
18
|
+
removedAt: string;
|
|
19
|
+
/** uid of whoever removed it. */
|
|
20
|
+
removedBy: string;
|
|
21
|
+
}
|
|
22
|
+
type RemovedFieldMap = {
|
|
23
|
+
readonly [key: string]: RemovedField;
|
|
24
|
+
};
|
|
25
|
+
/** Where a content type came from. Code always wins a slug conflict. See the registry. */
|
|
26
|
+
type ContentTypeSource = "code" | "database";
|
|
27
|
+
interface ContentType<TFields extends FieldMap = FieldMap> {
|
|
28
|
+
slug: string;
|
|
29
|
+
kind: ContentTypeKind;
|
|
30
|
+
label: string;
|
|
31
|
+
/** Plural label for a collection. Defaults to `label` when absent. */
|
|
32
|
+
labelPlural?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
/** Lucide icon name from the fixed vocabulary in .docs/styleguide/05-icons.md. */
|
|
35
|
+
icon?: string;
|
|
36
|
+
fields: TFields;
|
|
37
|
+
/**
|
|
38
|
+
* The order the fields are shown in, as a list of field keys.
|
|
39
|
+
*
|
|
40
|
+
* It exists because `fields` is a map, and object key order is not a place to store
|
|
41
|
+
* meaning: it is not a documented Firestore guarantee, Firestore's own ordering of map
|
|
42
|
+
* values compares keys sorted, and any re-serialization on the way through may change it.
|
|
43
|
+
* An array has one order and keeps it. A code-defined type never round trips and keeps its
|
|
44
|
+
* literal's order, so this key is absent there and the object order is used; the Schema
|
|
45
|
+
* Builder writes it on every save, which is what makes a reorder survive a reload.
|
|
46
|
+
*
|
|
47
|
+
* It is a hint, not a second source of truth. An unknown key in it is ignored and a field
|
|
48
|
+
* missing from it is appended, so a `fieldOrder` that has drifted from `fields` still
|
|
49
|
+
* renders every field exactly once.
|
|
50
|
+
*/
|
|
51
|
+
fieldOrder?: readonly string[];
|
|
52
|
+
/**
|
|
53
|
+
* Fields removed from `fields` whose stored values are still on the entries. Absent on a
|
|
54
|
+
* type that has never had a field removed.
|
|
55
|
+
*/
|
|
56
|
+
removedFields?: RemovedFieldMap;
|
|
57
|
+
/**
|
|
58
|
+
* A locked type cannot be edited or deleted through the Schema Builder, and the adapter
|
|
59
|
+
* rejects schema writes against it even when the UI is bypassed. Only meaningful for a
|
|
60
|
+
* code-defined type; a database-defined type is never locked.
|
|
61
|
+
*/
|
|
62
|
+
locked?: boolean;
|
|
63
|
+
/** Field key shown as the entry's title in lists and pickers. */
|
|
64
|
+
titleField?: keyof TFields & string;
|
|
65
|
+
/** Default sort for list views, as a field key with a direction. */
|
|
66
|
+
defaultSort?: {
|
|
67
|
+
field: keyof TFields & string;
|
|
68
|
+
direction: "asc" | "desc";
|
|
69
|
+
};
|
|
70
|
+
permissions?: PermissionMatrix;
|
|
71
|
+
/** Bumped when the stored shape changes. Read by the M10 migration runner. */
|
|
72
|
+
schemaVersion?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A content type with its field map erased to `FieldMap`.
|
|
76
|
+
*
|
|
77
|
+
* `ContentType<TFields>` is invariant in `TFields`, so a concrete content type produced by
|
|
78
|
+
* `defineContentType` is not assignable to `ContentType<FieldMap>`. Everything that handles
|
|
79
|
+
* content types generically (the registry, the adapter, the Schema Builder) takes this
|
|
80
|
+
* structural shape instead, which a concrete content type does satisfy. Authoring and
|
|
81
|
+
* inference keep the generic form.
|
|
82
|
+
*/
|
|
83
|
+
interface ContentTypeLike {
|
|
84
|
+
slug: string;
|
|
85
|
+
kind: ContentTypeKind;
|
|
86
|
+
label: string;
|
|
87
|
+
labelPlural?: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
icon?: string;
|
|
90
|
+
fields: FieldMap;
|
|
91
|
+
/**
|
|
92
|
+
* The order the fields are shown in, as a list of field keys.
|
|
93
|
+
*
|
|
94
|
+
* It exists because `fields` is a map and a map does not keep an order once it has been
|
|
95
|
+
* through Firestore: a stored document returns its map keys sorted, not in the order they
|
|
96
|
+
* were written. A code-defined type never round trips and keeps its literal's order, so
|
|
97
|
+
* this key is absent there and the object order is used; the Schema Builder writes it on
|
|
98
|
+
* every save, which is what makes a reorder survive a reload.
|
|
99
|
+
*
|
|
100
|
+
* It is a hint, not a second source of truth. An unknown key in it is ignored and a field
|
|
101
|
+
* missing from it is appended, so a `fieldOrder` that has drifted from `fields` still
|
|
102
|
+
* renders every field exactly once.
|
|
103
|
+
*/
|
|
104
|
+
fieldOrder?: readonly string[];
|
|
105
|
+
removedFields?: RemovedFieldMap;
|
|
106
|
+
locked?: boolean;
|
|
107
|
+
titleField?: string;
|
|
108
|
+
defaultSort?: {
|
|
109
|
+
field: string;
|
|
110
|
+
direction: "asc" | "desc";
|
|
111
|
+
};
|
|
112
|
+
permissions?: PermissionMatrix;
|
|
113
|
+
schemaVersion?: number;
|
|
114
|
+
}
|
|
115
|
+
/** Field keys the entry metadata already owns; a content type may not redefine them. */
|
|
116
|
+
declare const RESERVED_FIELD_KEYS: readonly ["id", "status", "createdAt", "createdBy", "updatedAt", "updatedBy", "publishedAt", "schemaVersion"];
|
|
117
|
+
type ReservedFieldKey = (typeof RESERVED_FIELD_KEYS)[number];
|
|
118
|
+
type ContentTypeValues<TType extends {
|
|
119
|
+
fields: FieldMap;
|
|
120
|
+
}> = FieldValues<TType["fields"]>;
|
|
121
|
+
type ContentTypeInputValues<TType extends {
|
|
122
|
+
fields: FieldMap;
|
|
123
|
+
}> = FieldInputValues<TType["fields"]>;
|
|
124
|
+
|
|
125
|
+
declare const ENTRY_STATUSES: readonly ["draft", "published", "archived"];
|
|
126
|
+
type EntryStatus = (typeof ENTRY_STATUSES)[number];
|
|
127
|
+
/**
|
|
128
|
+
* Everything the adapter owns on an entry. Written on every mutation, never by a form.
|
|
129
|
+
* Timestamps are ISO 8601 strings at this boundary; the Firestore converter turns them
|
|
130
|
+
* into Timestamps on the way in and back on the way out.
|
|
131
|
+
*/
|
|
132
|
+
interface EntryMeta {
|
|
133
|
+
id: string;
|
|
134
|
+
status: EntryStatus;
|
|
135
|
+
createdAt: string;
|
|
136
|
+
createdBy: string;
|
|
137
|
+
updatedAt: string;
|
|
138
|
+
updatedBy: string;
|
|
139
|
+
publishedAt: string | null;
|
|
140
|
+
schemaVersion: number;
|
|
141
|
+
}
|
|
142
|
+
/** What a read returns: the metadata plus the typed field values. */
|
|
143
|
+
type Entry<TType extends AnyContentTypeShape = AnyContentTypeShape> = EntryMeta & ContentTypeValues<TType>;
|
|
144
|
+
/** What a write accepts: required field values, optional ones omittable, no metadata. */
|
|
145
|
+
type EntryInput<TType extends AnyContentTypeShape = AnyContentTypeShape> = ContentTypeInputValues<TType> & {
|
|
146
|
+
status?: EntryStatus;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Structural stand-in for `ContentType<TFields>`. `ContentType` is invariant in its field
|
|
150
|
+
* map (it uses `keyof TFields` for titleField and defaultSort), so a concrete content type
|
|
151
|
+
* is not assignable to `ContentType<FieldMap>` and cannot be used as a constraint here.
|
|
152
|
+
*/
|
|
153
|
+
type AnyContentTypeShape = {
|
|
154
|
+
fields: FieldMap;
|
|
155
|
+
};
|
|
156
|
+
interface FieldError {
|
|
157
|
+
/** Dotted path, so a repeater reports `blocks.2.title` rather than `blocks`. */
|
|
158
|
+
path: string;
|
|
159
|
+
rule: string;
|
|
160
|
+
message: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { type AnyContentTypeShape as A, type ContentType as C, ENTRY_STATUSES as E, type FieldError as F, RESERVED_FIELD_KEYS as R, type ContentTypeInputValues as a, type ContentTypeKind as b, type ContentTypeLike as c, type ContentTypeSource as d, type ContentTypeValues as e, type Entry as f, type EntryInput as g, type EntryMeta as h, type EntryStatus as i, type RemovedField as j, type RemovedFieldMap as k, type ReservedFieldKey as l };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A reference from an entry to an item in the media library.
|
|
3
|
+
*
|
|
4
|
+
* Entries store the reference, not the media document, so renaming a file or editing its
|
|
5
|
+
* alt text does not require rewriting every entry that uses it. The denormalized `url` is
|
|
6
|
+
* a read convenience for the SDK and is refreshed by the adapter when the media item
|
|
7
|
+
* changes; `mediaId` is the source of truth.
|
|
8
|
+
*/
|
|
9
|
+
interface MediaReference {
|
|
10
|
+
mediaId: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
interface MediaItem {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
/** MIME type, for example `image/webp`. */
|
|
17
|
+
type: string;
|
|
18
|
+
/** Bytes. */
|
|
19
|
+
size: number;
|
|
20
|
+
url: string;
|
|
21
|
+
/** Storage object path, relative to the bucket root. */
|
|
22
|
+
path: string;
|
|
23
|
+
alt: string | null;
|
|
24
|
+
/** Pixels. Null for non-images. */
|
|
25
|
+
width: number | null;
|
|
26
|
+
height: number | null;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
createdBy: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "select", "date", "slug", "richtext", "image", "file", "relation", "repeater"];
|
|
32
|
+
type FieldType = (typeof FIELD_TYPES)[number];
|
|
33
|
+
/**
|
|
34
|
+
* The full rule vocabulary. Every rule compiles to the same runtime validator, which the
|
|
35
|
+
* admin form and the adapter both run, so a rule can never be enforced in one place only.
|
|
36
|
+
*/
|
|
37
|
+
interface ValidationRules {
|
|
38
|
+
required?: boolean;
|
|
39
|
+
minLength?: number;
|
|
40
|
+
maxLength?: number;
|
|
41
|
+
min?: number;
|
|
42
|
+
max?: number;
|
|
43
|
+
/** Serialized as a string, not a RegExp, because rules travel through Firestore. */
|
|
44
|
+
pattern?: string;
|
|
45
|
+
/** Rejects a value already held by another entry of the same content type. */
|
|
46
|
+
unique?: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface FieldCommon {
|
|
49
|
+
label: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
validation?: ValidationRules;
|
|
53
|
+
/** Hidden fields still store and validate; they are simply not rendered in the form. */
|
|
54
|
+
hidden?: boolean;
|
|
55
|
+
readonly?: boolean;
|
|
56
|
+
}
|
|
57
|
+
interface TextField extends FieldCommon {
|
|
58
|
+
type: "text";
|
|
59
|
+
defaultValue?: string;
|
|
60
|
+
}
|
|
61
|
+
interface TextareaField extends FieldCommon {
|
|
62
|
+
type: "textarea";
|
|
63
|
+
rows?: number;
|
|
64
|
+
defaultValue?: string;
|
|
65
|
+
}
|
|
66
|
+
interface NumberField extends FieldCommon {
|
|
67
|
+
type: "number";
|
|
68
|
+
step?: number;
|
|
69
|
+
defaultValue?: number;
|
|
70
|
+
}
|
|
71
|
+
interface BooleanField extends FieldCommon {
|
|
72
|
+
type: "boolean";
|
|
73
|
+
defaultValue?: boolean;
|
|
74
|
+
}
|
|
75
|
+
interface SelectOption<TValue extends string = string> {
|
|
76
|
+
value: TValue;
|
|
77
|
+
label: string;
|
|
78
|
+
}
|
|
79
|
+
interface SelectField<TValue extends string = string> extends FieldCommon {
|
|
80
|
+
type: "select";
|
|
81
|
+
options: readonly SelectOption<TValue>[];
|
|
82
|
+
multiple?: boolean;
|
|
83
|
+
defaultValue?: TValue;
|
|
84
|
+
}
|
|
85
|
+
interface DateField extends FieldCommon {
|
|
86
|
+
type: "date";
|
|
87
|
+
/** `date` stores `YYYY-MM-DD`; `datetime` stores a full ISO 8601 instant. */
|
|
88
|
+
precision?: "date" | "datetime";
|
|
89
|
+
}
|
|
90
|
+
interface SlugField extends FieldCommon {
|
|
91
|
+
type: "slug";
|
|
92
|
+
/** Field key the slug is derived from until the editor overrides it. */
|
|
93
|
+
from?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Rich text is stored as a serialized document, not as an HTML string, so the editor choice
|
|
97
|
+
* (settled in M5) stays an implementation detail and the value can be re-rendered rather
|
|
98
|
+
* than re-parsed.
|
|
99
|
+
*/
|
|
100
|
+
interface RichTextValue {
|
|
101
|
+
format: "husk-richtext-v1";
|
|
102
|
+
content: unknown;
|
|
103
|
+
}
|
|
104
|
+
interface RichtextField extends FieldCommon {
|
|
105
|
+
type: "richtext";
|
|
106
|
+
/** Marks and blocks the toolbar offers. Absent means the default set. */
|
|
107
|
+
features?: readonly string[];
|
|
108
|
+
}
|
|
109
|
+
interface ImageField extends FieldCommon {
|
|
110
|
+
type: "image";
|
|
111
|
+
accept?: readonly string[];
|
|
112
|
+
}
|
|
113
|
+
interface FileField extends FieldCommon {
|
|
114
|
+
type: "file";
|
|
115
|
+
accept?: readonly string[];
|
|
116
|
+
}
|
|
117
|
+
interface RelationField<TTo extends string = string> extends FieldCommon {
|
|
118
|
+
type: "relation";
|
|
119
|
+
/** Slug of the referenced content type. */
|
|
120
|
+
to: TTo;
|
|
121
|
+
many?: boolean;
|
|
122
|
+
}
|
|
123
|
+
interface RepeaterField<TFields extends FieldMap = FieldMap> extends FieldCommon {
|
|
124
|
+
type: "repeater";
|
|
125
|
+
fields: TFields;
|
|
126
|
+
min?: number;
|
|
127
|
+
max?: number;
|
|
128
|
+
}
|
|
129
|
+
type FieldDefinition = TextField | TextareaField | NumberField | BooleanField | SelectField | DateField | SlugField | RichtextField | ImageField | FileField | RelationField | RepeaterField;
|
|
130
|
+
type FieldMap = {
|
|
131
|
+
readonly [key: string]: FieldDefinition;
|
|
132
|
+
};
|
|
133
|
+
/** The value one field definition produces, before required-ness is applied. */
|
|
134
|
+
type FieldValue<TField extends FieldDefinition> = TField extends TextField | TextareaField | SlugField ? string : TField extends NumberField ? number : TField extends BooleanField ? boolean : TField extends DateField ? string : TField extends SelectField<infer TOption> ? TField extends {
|
|
135
|
+
multiple: true;
|
|
136
|
+
} ? TOption[] : TOption : TField extends RichtextField ? RichTextValue : TField extends ImageField | FileField ? MediaReference : TField extends RelationField ? TField extends {
|
|
137
|
+
many: true;
|
|
138
|
+
} ? string[] : string : TField extends RepeaterField<infer TFields> ? FieldValues<TFields>[] : never;
|
|
139
|
+
type IsRequired<TField extends FieldDefinition> = TField extends {
|
|
140
|
+
validation: {
|
|
141
|
+
required: true;
|
|
142
|
+
};
|
|
143
|
+
} ? true : false;
|
|
144
|
+
/**
|
|
145
|
+
* Read shape: every key is present. An optional field reads as `null` rather than
|
|
146
|
+
* `undefined`, so a consumer never has to distinguish "absent" from "empty".
|
|
147
|
+
*/
|
|
148
|
+
type FieldValues<TFields extends FieldMap> = {
|
|
149
|
+
[K in keyof TFields]: IsRequired<TFields[K]> extends true ? FieldValue<TFields[K]> : FieldValue<TFields[K]> | null;
|
|
150
|
+
};
|
|
151
|
+
/** Write shape: optional fields may be omitted entirely. */
|
|
152
|
+
type FieldInputValues<TFields extends FieldMap> = {
|
|
153
|
+
[K in keyof TFields as IsRequired<TFields[K]> extends true ? K : never]: FieldValue<TFields[K]>;
|
|
154
|
+
} & {
|
|
155
|
+
[K in keyof TFields as IsRequired<TFields[K]> extends true ? never : K]?: FieldValue<TFields[K]> | null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export { type BooleanField as B, type DateField as D, FIELD_TYPES as F, type ImageField as I, type MediaItem as M, type NumberField as N, type RelationField as R, type SelectField as S, type TextField as T, type ValidationRules as V, type FieldDefinition as a, type FieldInputValues as b, type FieldMap as c, type FieldType as d, type FieldValue as e, type FieldValues as f, type FileField as g, type MediaReference as h, type RepeaterField as i, type RichTextValue as j, type RichtextField as k, type SelectOption as l, type SlugField as m, type TextareaField as n };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
import { F as FieldError, A as AnyContentTypeShape } from './entry-D3kiE8T1.js';
|
|
3
|
+
import { a as FieldDefinition, d as FieldType, c as FieldMap, V as ValidationRules } from './field-Diyf5siF.js';
|
|
4
|
+
|
|
5
|
+
/** Props every admin field input receives. Field types never read from a store directly. */
|
|
6
|
+
interface FieldInputProps<TValue, TField extends FieldDefinition = FieldDefinition> {
|
|
7
|
+
/** Dotted path, unique within the form: `title`, `blocks.2.heading`. */
|
|
8
|
+
path: string;
|
|
9
|
+
field: TField;
|
|
10
|
+
value: TValue | null;
|
|
11
|
+
onChange: (value: TValue | null) => void;
|
|
12
|
+
onBlur?: (() => void) | undefined;
|
|
13
|
+
errors: readonly FieldError[];
|
|
14
|
+
disabled?: boolean | undefined;
|
|
15
|
+
readonly?: boolean | undefined;
|
|
16
|
+
}
|
|
17
|
+
type FieldInputComponent<TValue, TField extends FieldDefinition = FieldDefinition> = ComponentType<FieldInputProps<TValue, TField>>;
|
|
18
|
+
/** Everything a validator may know without touching the database. */
|
|
19
|
+
interface ValidateCtx {
|
|
20
|
+
path: string;
|
|
21
|
+
contentType: AnyContentTypeShape;
|
|
22
|
+
/** All values of the entry being validated, for cross-field rules. */
|
|
23
|
+
entry: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Recursion, for the two field types that contain other fields.
|
|
27
|
+
*
|
|
28
|
+
* `repeater` holds a whole field map, and it cannot convert or name what is inside it
|
|
29
|
+
* without applying the same field type contract one level down. It must not reach for the
|
|
30
|
+
* registry itself: the registry imports every spec, so a spec that imports the registry
|
|
31
|
+
* closes a cycle. The caller that already holds the registry passes the recursion in
|
|
32
|
+
* instead, and both members below are optional, so a spec that nests nothing ignores them
|
|
33
|
+
* and a caller that has no registry still gets a working round trip.
|
|
34
|
+
*/
|
|
35
|
+
interface NestedConverter {
|
|
36
|
+
toStored(fields: FieldMap, values: Record<string, unknown>): Record<string, unknown>;
|
|
37
|
+
fromStored(fields: FieldMap, data: Record<string, unknown>): Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/** Names the SDK-facing type of any field definition, one level down. */
|
|
40
|
+
type TsTypeResolver = (field: FieldDefinition) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Layer 5, as a value or as a function of the field.
|
|
43
|
+
*
|
|
44
|
+
* A plain string is the common case and stays exactly as it was. A function is what lets a
|
|
45
|
+
* field type whose SDK type depends on its own options say so: `select` narrows on
|
|
46
|
+
* `multiple`, `relation` on `many`, and `repeater` names the object its rows actually
|
|
47
|
+
* hold. `FieldValue<TField>` in types/field.ts already narrows all three for a code-defined
|
|
48
|
+
* type; this is what codegen can say for a type created in the Schema Builder, where the
|
|
49
|
+
* field definition exists only at runtime.
|
|
50
|
+
*/
|
|
51
|
+
type FieldTsType<TField extends FieldDefinition = FieldDefinition> = string | ((field: TField, resolve: TsTypeResolver) => string);
|
|
52
|
+
/**
|
|
53
|
+
* How one per-type option is edited in the Schema Builder.
|
|
54
|
+
*
|
|
55
|
+
* A control, not a field type: `"choice"` is a fixed list of values, `"content-type"` is a
|
|
56
|
+
* slug picked from the live registry, `"field-key"` is a sibling field of the same level.
|
|
57
|
+
* The field editor implements this small vocabulary once, and every field type describes
|
|
58
|
+
* its own options in it, which is what keeps `switch (field.type)` out of the screen.
|
|
59
|
+
*/
|
|
60
|
+
type FieldOptionControl = "text" | "number" | "boolean" | "choice" | "select-options" | "content-type" | "field-key" | "string-list" | "fields";
|
|
61
|
+
interface FieldOptionDescriptor {
|
|
62
|
+
/** The property this option writes on the field definition. */
|
|
63
|
+
key: string;
|
|
64
|
+
label: string;
|
|
65
|
+
control: FieldOptionControl;
|
|
66
|
+
help?: string;
|
|
67
|
+
/** A field of this type is incomplete until the option has a value. */
|
|
68
|
+
required?: boolean;
|
|
69
|
+
/** The values a `"choice"` control offers. */
|
|
70
|
+
choices?: readonly {
|
|
71
|
+
value: string;
|
|
72
|
+
label: string;
|
|
73
|
+
}[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Layer 7: what the Schema Builder may configure on this field type, and what it starts as.
|
|
77
|
+
*
|
|
78
|
+
* It exists so that the field editor is generated from the registry exactly as every other
|
|
79
|
+
* admin screen is. Without it, one screen would hold a list of which options belong to
|
|
80
|
+
* which field type, and a field type added later would render as a bare label until
|
|
81
|
+
* somebody remembered to edit that list.
|
|
82
|
+
*/
|
|
83
|
+
interface FieldEditorSpec {
|
|
84
|
+
/** Name shown in the field type picker. */
|
|
85
|
+
label: string;
|
|
86
|
+
/** One line, shown under the name in the picker. */
|
|
87
|
+
description: string;
|
|
88
|
+
/** The shared validation rules this field type honours. See VALIDATION_RULE_CONTROLS. */
|
|
89
|
+
rules: readonly (keyof ValidationRules)[];
|
|
90
|
+
/** The per-type options, in the order the editor shows them. */
|
|
91
|
+
options: readonly FieldOptionDescriptor[];
|
|
92
|
+
/**
|
|
93
|
+
* Everything a new field of this type needs besides `type` and `label`.
|
|
94
|
+
*
|
|
95
|
+
* A select with no options and a repeater with no nested fields are both invalid, so a
|
|
96
|
+
* newly added field cannot start as `{ type, label }` alone.
|
|
97
|
+
*/
|
|
98
|
+
defaults: Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The seven-layer contract. A field type is not "added" until every member here is
|
|
102
|
+
* implemented; a partial field type is a field type that behaves differently in the form
|
|
103
|
+
* than it does on the server.
|
|
104
|
+
*
|
|
105
|
+
* 1. `name` schema definition: the discriminator a content type writes
|
|
106
|
+
* 2. `toStored` /
|
|
107
|
+
* `fromStored` stored shape: the round trip through Firestore
|
|
108
|
+
* 3. `Input` admin input component
|
|
109
|
+
* 4. `validate` validation, run identically by the form and by the adapter
|
|
110
|
+
* 5. `tsType` SDK-facing type, used by codegen and by the Developer area
|
|
111
|
+
* 6. `defaultValue` registration data: what a new entry starts with
|
|
112
|
+
* 7. `editor` Schema Builder data: the options, rules and defaults of the type
|
|
113
|
+
*/
|
|
114
|
+
interface FieldTypeSpec<TValue, TField extends FieldDefinition = FieldDefinition> {
|
|
115
|
+
name: FieldType;
|
|
116
|
+
toStored(value: TValue | null, field: TField, nested?: NestedConverter): unknown;
|
|
117
|
+
fromStored(raw: unknown, field: TField, nested?: NestedConverter): TValue | null;
|
|
118
|
+
Input: FieldInputComponent<TValue, TField>;
|
|
119
|
+
validate(value: TValue | null, rules: ValidationRules, ctx: ValidateCtx, field: TField): FieldError[];
|
|
120
|
+
tsType: FieldTsType<TField>;
|
|
121
|
+
defaultValue(field: TField): TValue | null;
|
|
122
|
+
editor: FieldEditorSpec;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A spec whose value and field types have been erased, for storage in the registry.
|
|
126
|
+
*
|
|
127
|
+
* The registry is heterogeneous by construction: one spec per field type, each with a
|
|
128
|
+
* different value type. The erasure stops at this alias; every consumer goes back through
|
|
129
|
+
* the typed `FieldTypeSpec` for the field it is actually handling.
|
|
130
|
+
*/
|
|
131
|
+
type AnyFieldTypeSpec = FieldTypeSpec<any, any>;
|
|
132
|
+
interface FieldTypeRegistry {
|
|
133
|
+
get(name: string): AnyFieldTypeSpec | undefined;
|
|
134
|
+
require(name: string): AnyFieldTypeSpec;
|
|
135
|
+
has(name: string): boolean;
|
|
136
|
+
readonly names: readonly FieldType[];
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Builds the single registration point for field types.
|
|
140
|
+
*
|
|
141
|
+
* The admin never switches on a field name: it looks the spec up here and renders
|
|
142
|
+
* `spec.Input`. If a screen needs `switch (field.type)`, the abstraction has failed.
|
|
143
|
+
*/
|
|
144
|
+
declare function registerFieldTypes(specs: readonly AnyFieldTypeSpec[]): FieldTypeRegistry;
|
|
145
|
+
/** What codegen writes for a field whose type nothing has registered. */
|
|
146
|
+
declare const UNKNOWN_TS_TYPE = "unknown";
|
|
147
|
+
/**
|
|
148
|
+
* Layer 5 for one field definition, resolved through the registry.
|
|
149
|
+
*
|
|
150
|
+
* The one place that knows a `tsType` may be a function, so no caller has to branch on it,
|
|
151
|
+
* and the one place that supplies the recursion a nesting field type asks for. An
|
|
152
|
+
* unregistered field type resolves to `unknown` rather than throwing: codegen for a
|
|
153
|
+
* database-defined content type must not fail on one field it does not recognise.
|
|
154
|
+
*/
|
|
155
|
+
declare function resolveTsType(registry: FieldTypeRegistry, field: FieldDefinition): string;
|
|
156
|
+
|
|
157
|
+
export { type AnyFieldTypeSpec as A, type FieldEditorSpec as F, type NestedConverter as N, type TsTypeResolver as T, UNKNOWN_TS_TYPE as U, type ValidateCtx as V, type FieldInputComponent as a, type FieldInputProps as b, type FieldOptionControl as c, type FieldOptionDescriptor as d, type FieldTsType as e, type FieldTypeRegistry as f, type FieldTypeSpec as g, resolveTsType as h, registerFieldTypes as r };
|