@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/LICENSE +166 -0
  3. package/README.md +140 -0
  4. package/api/public-api.md +1070 -0
  5. package/dist/adapter-C_0mhENy.d.ts +320 -0
  6. package/dist/admin/admin.css +5 -0
  7. package/dist/admin/index.d.ts +265 -0
  8. package/dist/admin/index.js +393 -0
  9. package/dist/admin/index.js.map +1 -0
  10. package/dist/admin/tokens.css +597 -0
  11. package/dist/admin/ui.d.ts +3013 -0
  12. package/dist/admin/ui.js +9783 -0
  13. package/dist/admin/ui.js.map +1 -0
  14. package/dist/auth/index.d.ts +4 -0
  15. package/dist/auth/index.js +9 -0
  16. package/dist/auth/index.js.map +1 -0
  17. package/dist/auth-B2gBtHl5.d.ts +28 -0
  18. package/dist/can-Chy7zmx7.d.ts +69 -0
  19. package/dist/chunk-3I6337WJ.js +83 -0
  20. package/dist/chunk-3I6337WJ.js.map +1 -0
  21. package/dist/chunk-4Z6P3XCE.js +62 -0
  22. package/dist/chunk-4Z6P3XCE.js.map +1 -0
  23. package/dist/chunk-6E3ZS5Z5.js +1007 -0
  24. package/dist/chunk-6E3ZS5Z5.js.map +1 -0
  25. package/dist/chunk-77JDRXXU.js +3 -0
  26. package/dist/chunk-77JDRXXU.js.map +1 -0
  27. package/dist/chunk-FHYWJCG6.js +171 -0
  28. package/dist/chunk-FHYWJCG6.js.map +1 -0
  29. package/dist/chunk-JAE7HHMM.js +506 -0
  30. package/dist/chunk-JAE7HHMM.js.map +1 -0
  31. package/dist/chunk-NPDBQBLW.js +68 -0
  32. package/dist/chunk-NPDBQBLW.js.map +1 -0
  33. package/dist/chunk-ORMYARXD.js +6 -0
  34. package/dist/chunk-ORMYARXD.js.map +1 -0
  35. package/dist/chunk-R4O7APIX.js +51 -0
  36. package/dist/chunk-R4O7APIX.js.map +1 -0
  37. package/dist/chunk-ROLKRJWI.js +187 -0
  38. package/dist/chunk-ROLKRJWI.js.map +1 -0
  39. package/dist/chunk-SOTQ3WP5.js +31 -0
  40. package/dist/chunk-SOTQ3WP5.js.map +1 -0
  41. package/dist/chunk-TCFFHTY3.js +97 -0
  42. package/dist/chunk-TCFFHTY3.js.map +1 -0
  43. package/dist/chunk-TS7Y7GEZ.js +7 -0
  44. package/dist/chunk-TS7Y7GEZ.js.map +1 -0
  45. package/dist/chunk-VLGR5PZX.js +3 -0
  46. package/dist/chunk-VLGR5PZX.js.map +1 -0
  47. package/dist/chunk-ZW2PUSVS.js +40 -0
  48. package/dist/chunk-ZW2PUSVS.js.map +1 -0
  49. package/dist/chunk-ZYNBMYJ6.js +392 -0
  50. package/dist/chunk-ZYNBMYJ6.js.map +1 -0
  51. package/dist/cli/nonext-husk.js +3360 -0
  52. package/dist/cli/nonext-husk.js.map +1 -0
  53. package/dist/cli/templates/firestore.rules +393 -0
  54. package/dist/cli/templates/storage.rules +165 -0
  55. package/dist/client/index.d.ts +27 -0
  56. package/dist/client/index.js +15 -0
  57. package/dist/client/index.js.map +1 -0
  58. package/dist/cms-9fSA3uQV.d.ts +158 -0
  59. package/dist/core/index.d.ts +1267 -0
  60. package/dist/core/index.js +12 -0
  61. package/dist/core/index.js.map +1 -0
  62. package/dist/entry-D3kiE8T1.d.ts +163 -0
  63. package/dist/field-Diyf5siF.d.ts +158 -0
  64. package/dist/field-type-D25Vojb0.d.ts +157 -0
  65. package/dist/firebase/index.d.ts +251 -0
  66. package/dist/firebase/index.js +991 -0
  67. package/dist/firebase/index.js.map +1 -0
  68. package/dist/index-5ylaMBUH.d.ts +155 -0
  69. package/dist/index.d.ts +14 -0
  70. package/dist/index.js +17 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/permissions/index.d.ts +101 -0
  73. package/dist/permissions/index.js +7 -0
  74. package/dist/permissions/index.js.map +1 -0
  75. package/dist/registry-CUC25qay.d.ts +59 -0
  76. package/dist/richtext/index.d.ts +85 -0
  77. package/dist/richtext/index.js +127 -0
  78. package/dist/richtext/index.js.map +1 -0
  79. package/dist/schema/index.d.ts +50 -0
  80. package/dist/schema/index.js +5 -0
  81. package/dist/schema/index.js.map +1 -0
  82. package/dist/server/index.d.ts +39 -0
  83. package/dist/server/index.js +25 -0
  84. package/dist/server/index.js.map +1 -0
  85. package/dist/types/index.d.ts +36 -0
  86. package/dist/types/index.js +6 -0
  87. package/dist/types/index.js.map +1 -0
  88. package/dist/write-limits-Djmm6L3R.d.ts +215 -0
  89. package/package.json +136 -0
@@ -0,0 +1,1070 @@
1
+ # `@nonext/husk` public API
2
+
3
+ Generated by `pnpm --filter @nonext/husk api`. Do not edit by hand.
4
+
5
+ Every line here is a promise to every consuming project. A diff in this file is a public
6
+ API change and needs a changeset saying whether it is patch, minor, or breaking.
7
+ `packages/cms/src/public-api.test.ts` fails while this file and the source disagree, and
8
+ the `api-review` CI job fails when a pull request changes it without a changeset.
9
+
10
+ **976 exported names across 12 entry points.**
11
+
12
+ ## `@nonext/husk`
13
+
14
+ 323 exports.
15
+
16
+ ```ts
17
+ export type Action = EntryAction | AdminAction
18
+ const ACTIONS: ReadonlyArray<Action>
19
+ function addField: (type: ContentTypeLike, key: string, field: FieldDefinition) => ContentTypeLike
20
+ const ADMIN_ACTIONS: readonly ["manage-schema", "manage-users", "manage-settings", "manage-media"]
21
+ export type AdminAction = "manage-schema" | "manage-users" | "manage-settings" | "manage-media"
22
+ const ALLOWED_MEDIA_TYPES: ReadonlyArray<MediaTypeRule>
23
+ export type AnyContentTypeShape = { fields: FieldMap }
24
+ export type AnyFieldTypeSpec = FieldTypeSpec<any, any>
25
+ function applyPlan: (plan: readonly EntryMigration[], values: Record<string, unknown>, context: Omit<MigrationContext, "fromVersion">) => Record<string, unknown>
26
+ function approximateBytes: (value: unknown) => number
27
+ function assertDeletable: (type: KindShape) => void
28
+ function assertListable: (type: KindShape) => void
29
+ function assertSchemaWritable: (registry: Registry, slug: string) => RegistryEntry
30
+ function assertSingletonCreatable: (type: KindShape, exists: boolean) => void
31
+ function assertSingletonEntryId: (type: KindShape, entryId: string) => void
32
+ function assertTypeSlugAvailable: (registry: Registry, slug: string) => void
33
+ function assertValidContentType: (definition: { slug: string; kind: unknown; fields: FieldMap; removedFields?: RemovedFieldMap | undefined; titleField?: string | undefined; defaultSort?: { field: string; direction: "asc" | "desc"; } | undefined; permissions?: unknown; }) => void
34
+ function assertValidPermissionMatrix: (slug: string, matrix: unknown) => void
35
+ export interface AuthUser { uid: string email: string displayName: string | null role: Role disabled: boolean }
36
+ export interface BooleanField extends FieldCommon { type: "boolean" defaultValue?: boolean }
37
+ function buildCountQuery: (type: ContentTypeLike, state: ListViewState) => Pick<ListQuery, "filters" | "status" | "search">
38
+ function buildListQuery: (type: ContentTypeLike, state: ListViewState, { pageSize }?: BuildListQueryOptions) => ListQuery
39
+ export interface BuildListQueryOptions { pageSize?: number }
40
+ function buildRegistry: (code: readonly ContentTypeLike[], database: readonly ContentTypeLike[]) => Registry
41
+ function can: (user: AuthUser, action: Action, context?: PermissionContext) => boolean
42
+ function canSaveEntry: (user: AuthUser, type: ContentTypeLike, entry: StatusSubject | null) => boolean
43
+ function canSearch: (type: ContentTypeLike) => boolean
44
+ function checkFieldKey: (type: ContentTypeLike, key: string) => FieldKeyAvailability
45
+ function checkMediaFile: (file: MediaFileFacts) => MediaRejection | null
46
+ function checkNestedFieldKey: (fields: FieldMap, key: string) => FieldKeyAvailability
47
+ function checkTypeSlug: (registry: Registry, slug: string) => SlugAvailability
48
+ function checkWritePayload: (values: Record<string, unknown>, limits?: PayloadLimits) => WriteLimitRejection | null
49
+ export interface ClassifiedError { category: ErrorCategory treatment: ErrorTreatment title: string message: string fieldErrors: readonly FieldError[] retryable: boolean cause: unknown }
50
+ function classifyError: (thrown: unknown, context?: ErrorContext) => ClassifiedError
51
+ export type Clock = () => number
52
+ export interface Cms<TTypes extends ContentTypeMap = ContentTypeMap> { collection<TSlug extends SlugOf<TTypes>>( slug: TSlug, ): CollectionReader<EntryOf<TTypes, TSlug>> single<TSlug extends SlugOf<TTypes>>(slug: TSlug): SingleReader<EntryOf<TTypes, TSlug>> media(): MediaReader settings(): SettingsReader readonly isPreview: boolean }
53
+ export interface CmsAdapter { list(slug: string, query?: ListQuery): Promise<Page<Entry>> get(slug: string, entryId: string): Promise<Entry | null> getBySlug( slug: string, value: string, options?: Pick<ListQuery, "status">, ): Promise<Entry | null> getSingle(slug: string): Promise<Entry | null> create(slug: string, input: EntryInputData): Promise<Entry> update(slug: string, entryId: string, input: EntryInputData): Promise<Entry> delete(slug: string, entryId: string): Promise<void> count( slug: string, query?: Pick<ListQuery, "filters" | "status" | "search">, ): Promise<number> readonly media: MediaAdapter readonly schemas: SchemaAdapter readonly fieldData: FieldDataAdapter readonly migrations: MigrationAdapter readonly settings: SettingsAdapter }
54
+ export interface CmsConfig< TTypes extends ContentTypeMap = ContentTypeMap, > extends CmsReadConfig<TTypes> { preview?: boolean }
55
+ export type CmsCountQuery = Pick<CmsQuery, "filters" | "search">
56
+ export type CmsQuery = Omit<ListQuery, "status">
57
+ export interface CmsReadConfig<TTypes extends ContentTypeMap = ContentTypeMap> { adapter: CmsAdapter contentTypes?: TTypes }
58
+ export interface CollectionReader<TEntry = Entry> { findMany(query?: CmsQuery): Promise<Page<TEntry>> findBySlug(value: string): Promise<TEntry | null> findById(entryId: string): Promise<TEntry | null> count(query?: CmsCountQuery): Promise<number> }
59
+ function collectMediaIds: (data: Record<string, unknown>) => string[]
60
+ export type ColumnAlign = "text" | "numeric" | "date"
61
+ export type ColumnKind = "title" | "field" | "status" | "updated"
62
+ function confirmationMatches: (expected: string, typed: string) => boolean
63
+ export interface ContentType<TFields extends FieldMap = FieldMap> { slug: string kind: ContentTypeKind label: string labelPlural?: string description?: string icon?: string fields: TFields fieldOrder?: readonly string[] removedFields?: RemovedFieldMap locked?: boolean titleField?: keyof TFields & string defaultSort?: { field: keyof TFields & string; direction: "asc" | "desc" } permissions?: PermissionMatrix schemaVersion?: number }
64
+ export type ContentTypeInputValues<TType extends { fields: FieldMap }> = FieldInputValues< TType["fields"] >
65
+ export type ContentTypeKind = "collection" | "singleton"
66
+ export interface ContentTypeLike { slug: string kind: ContentTypeKind label: string labelPlural?: string description?: string icon?: string fields: FieldMap fieldOrder?: readonly string[] removedFields?: RemovedFieldMap locked?: boolean titleField?: string defaultSort?: { field: string; direction: "asc" | "desc" } permissions?: PermissionMatrix schemaVersion?: number }
67
+ export type ContentTypeMap = { readonly [slug: string]: KindShape & AnyContentTypeShape }
68
+ export type ContentTypeSource = "code" | "database"
69
+ function contentTypeTag: (slug: string) => string
70
+ export type ContentTypeValues<TType extends { fields: FieldMap }> = FieldValues< TType["fields"] >
71
+ function createCMS: <TTypes extends ContentTypeMap = ContentTypeMap>(config: CmsConfig<TTypes>) => Cms<TTypes>
72
+ function createContentType: (registry: Registry, input: NewContentTypeInput) => ContentTypeLike
73
+ function createMigrationRegistry: (migrations: readonly EntryMigration[]) => MigrationRegistry
74
+ function createRegistryStore: (code: readonly ContentTypeLike[], initialDatabase?: readonly ContentTypeLike[]) => RegistryStore
75
+ function createWriteRateLimiter: (clock?: Clock) => WriteRateLimiter
76
+ export interface DateField extends FieldCommon { type: "date" precision?: "date" | "datetime" }
77
+ export type Decision = "allow" | "deny" | "own-entry-only"
78
+ const DEFAULT_PAGE_SIZE: 25
79
+ function defaultListSort: (type: ContentTypeLike) => SortSpec
80
+ function defaultRoles: (action: EntryAction) => readonly Role[]
81
+ function defineContentType: <const TFields extends FieldMap>(definition: ContentType<TFields>) => ContentType<TFields>
82
+ function deleteConfirmationName: (type: ContentTypeLike, entry: Entry) => string
83
+ function deleteConfirmationPhrase: (type: ContentTypeLike, count: number) => string
84
+ function deniedError: (what: string, role: Role | undefined) => ClassifiedError
85
+ function draftIssues: (registry: FieldTypeRegistry, rows: readonly FieldRow[]) => readonly (FieldOptionIssue & { key: string; })[]
86
+ export type Entry<TType extends AnyContentTypeShape = AnyContentTypeShape> = EntryMeta & ContentTypeValues<TType>
87
+ const ENTRY_ACTIONS: readonly ["read", "create", "update", "delete"]
88
+ const ENTRY_COLLECTION_GROUP: "entries"
89
+ const ENTRY_STATUSES: readonly ["draft", "published", "archived"]
90
+ export type EntryAction = "read" | "create" | "update" | "delete"
91
+ function entryChangedSince: (baseline: Pick<EntryMeta, "updatedAt">, current: Pick<EntryMeta, "updatedAt">) => boolean
92
+ export interface EntryFilter { field: string op: FilterOperator value: unknown }
93
+ export type EntryInput<TType extends AnyContentTypeShape = AnyContentTypeShape> = ContentTypeInputValues<TType> & { status?: EntryStatus }
94
+ export interface EntryInputData { values: Record<string, unknown> status?: EntryStatus }
95
+ export interface EntryMeta { id: string status: EntryStatus createdAt: string createdBy: string updatedAt: string updatedBy: string publishedAt: string | null schemaVersion: number }
96
+ export interface EntryMigration { slug: string from: number to: number describe: string migrate( values: Record<string, unknown>, context: MigrationContext, ): Record<string, unknown> }
97
+ export type EntryOf< TTypes extends ContentTypeMap, TSlug extends string, > = TSlug extends keyof TTypes ? Entry<TTypes[TSlug]> : Entry
98
+ export type EntryOwnership = Pick<EntryMeta, "createdBy">
99
+ function entrySlugTag: (slug: string, value: string) => string
100
+ export type EntryStatus = (typeof ENTRY_STATUSES)[number]
101
+ function entryTag: (slug: string, entryId: string) => string
102
+ function entryTags: (slug: string, entryId: string) => readonly string[]
103
+ function entryTitle: (type: ContentTypeLike, entry: Entry) => string
104
+ function entryUniqueCheck: (options: EntryUniqueOptions) => (path: string, value: string) => Promise<boolean>
105
+ export interface EntryUniqueOptions { adapter: CmsAdapter type: ContentTypeLike entryId: string | null }
106
+ const ERROR_CATEGORIES: readonly ["validation", "permission", "not-found", "conflict", "transport", "unknown"]
107
+ export type ErrorCategory = (typeof ERROR_CATEGORIES)[number]
108
+ export interface ErrorContext { role?: Role | undefined action?: string | undefined subject?: string | undefined }
109
+ export type ErrorOrigin = "client" | "server"
110
+ function errorsUnder: <TError extends FieldError>(errors: readonly TError[], path: string) => TError[]
111
+ export type ErrorTreatment = "field" | "banner" | "state" | "resolve" | "retry"
112
+ const FALLBACK_SORT: SortSpec
113
+ const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "select", "date", "slug", "richtext", "image", "file", "relation", "repeater"]
114
+ export interface FieldDataAdapter { count(slug: string, key: string): Promise<number> purge(slug: string, key: string, options?: FieldPurgeOptions): Promise<FieldPurgeResult> }
115
+ export type FieldDefinition = | TextField | TextareaField | NumberField | BooleanField | SelectField | DateField | SlugField | RichtextField | ImageField | FileField | RelationField | RepeaterField
116
+ export interface FieldEditorSpec { label: string description: string rules: readonly (keyof ValidationRules)[] options: readonly FieldOptionDescriptor[] defaults: Record<string, unknown> }
117
+ export interface FieldError { path: string rule: string message: string }
118
+ export type FieldInputComponent< TValue, TField extends FieldDefinition = FieldDefinition, > = ComponentType<FieldInputProps<TValue, TField>>
119
+ export interface FieldInputProps< TValue, TField extends FieldDefinition = FieldDefinition, > { path: string field: TField value: TValue | null onChange: (value: TValue | null) => void onBlur?: (() => void) | undefined errors: readonly FieldError[] disabled?: boolean | undefined readonly?: boolean | undefined }
120
+ export type FieldInputValues<TFields extends FieldMap> = { [K in keyof TFields as IsRequired<TFields[K]> extends true ? K : never]: FieldValue< TFields[K] > } & { [K in keyof TFields as IsRequired<TFields[K]> extends true ? never : K]?: FieldValue< TFields[K] > | null }
121
+ export interface FieldKeyAvailability { available: boolean reason?: FieldKeyRejection message?: string }
122
+ export type FieldKeyRejection = "invalid" | "reserved" | "live" | "removed"
123
+ function fieldKeyState: (type: ContentTypeLike, key: string) => FieldKeyState
124
+ export type FieldKeyState = "free" | "live" | "removed"
125
+ export type FieldMap = { readonly [key: string]: FieldDefinition }
126
+ function fieldMapSignature: (fields: FieldMap) => string
127
+ export type FieldOptionControl = | "text" | "number" | "boolean" | "choice" | "select-options" | "content-type" | "field-key" | "string-list" | "fields"
128
+ export interface FieldOptionDescriptor { key: string label: string control: FieldOptionControl help?: string required?: boolean choices?: readonly { value: string; label: string }[] }
129
+ export interface FieldOptionIssue { option: string message: string }
130
+ function fieldOptionIssues: (registry: FieldTypeRegistry, field: FieldDefinition) => readonly FieldOptionIssue[]
131
+ export interface FieldPurgeOptions { limit?: number }
132
+ export interface FieldPurgeResult { purged: number remaining: boolean }
133
+ export interface FieldRow { key: string field: FieldDefinition }
134
+ export type FieldTsType<TField extends FieldDefinition = FieldDefinition> = string | ((field: TField, resolve: TsTypeResolver) => string)
135
+ export type FieldType = (typeof FIELD_TYPES)[number]
136
+ export interface FieldTypeRegistry { get(name: string): AnyFieldTypeSpec | undefined require(name: string): AnyFieldTypeSpec has(name: string): boolean readonly names: readonly FieldType[] }
137
+ export interface FieldTypeSpec<TValue, TField extends FieldDefinition = FieldDefinition> { name: FieldType toStored(value: TValue | null, field: TField, nested?: NestedConverter): unknown fromStored(raw: unknown, field: TField, nested?: NestedConverter): TValue | null Input: FieldInputComponent<TValue, TField> validate( value: TValue | null, rules: ValidationRules, ctx: ValidateCtx, field: TField, ): FieldError[] tsType: FieldTsType<TField> defaultValue(field: TField): TValue | null editor: FieldEditorSpec }
138
+ export 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 { multiple: true } ? TOption[] : TOption : TField extends RichtextField ? RichTextValue : TField extends ImageField | FileField ? MediaReference : TField extends RelationField ? TField extends { many: true } ? string[] : string : TField extends RepeaterField<infer TFields> ? FieldValues<TFields>[] : never
139
+ export type FieldValues<TFields extends FieldMap> = { [K in keyof TFields]: IsRequired<TFields[K]> extends true ? FieldValue<TFields[K]> : FieldValue<TFields[K]> | null }
140
+ function fileExtension: (name: string) => string
141
+ export interface FileField extends FieldCommon { type: "file" accept?: readonly string[] }
142
+ export type FilterOperator = | "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "array-contains" | "array-contains-any"
143
+ const FIRESTORE_DOCUMENT_BYTES: 1048576
144
+ export interface FirestoreIndex { collectionGroup: string queryScope: "COLLECTION" fields: readonly FirestoreIndexField[] }
145
+ export interface FirestoreIndexField { fieldPath: string order: IndexOrder }
146
+ function forgetRemovedField: (type: ContentTypeLike, key: string) => ContentTypeLike
147
+ export interface FormField { key: string field: FieldDefinition }
148
+ function formFields: (type: ContentTypeLike) => readonly FormField[]
149
+ export interface FormIssue extends FieldError { origin: ErrorOrigin }
150
+ function formValuesFromEntry: (type: ContentTypeLike, entry: Entry, registry: FieldTypeRegistry) => Record<string, unknown>
151
+ const HUSK_TAG_PREFIX: "husk"
152
+ export interface HuskConfig { generatedWith: string adminRoute: string projectId: string storageBucket: string media: boolean auth: boolean emulators: boolean }
153
+ export interface ImageField extends FieldCommon { type: "image" accept?: readonly string[] }
154
+ function indexCovered: (file: { indexes?: readonly FirestoreIndex[]; }, required: FirestoreIndex) => boolean
155
+ export type IndexOrder = "ASCENDING" | "DESCENDING"
156
+ const INITIAL_FIELD_KEY: "title"
157
+ const INITIAL_SCHEMA_VERSION: 1
158
+ function initialFormValues: (type: ContentTypeLike, registry: FieldTypeRegistry) => Record<string, unknown>
159
+ function initialListState: (type: ContentTypeLike) => ListViewState
160
+ export interface InvalidContentType { slug: string reason: string definition: unknown }
161
+ function isCollection: (type: KindShape) => boolean
162
+ function isEntryAction: (action: Action) => action is EntryAction
163
+ function isFilterableField: (field: FieldDefinition) => boolean
164
+ function isHuskTag: (value: string) => boolean
165
+ function isNarrowed: (state: ListViewState) => boolean
166
+ function isSchemaReadOnly: (entry: RegistryEntry) => boolean
167
+ function isSearching: (state: ListViewState) => boolean
168
+ function isSingleton: (type: KindShape) => boolean
169
+ export type KnownSlug<TTypes extends ContentTypeMap> = keyof TTypes & string
170
+ export interface ListColumn { key: string label: string kind: ColumnKind align: ColumnAlign sortable: boolean }
171
+ export interface ListColumnOptions { maxFieldColumns?: number }
172
+ function listColumns: (type: ContentTypeLike, { maxFieldColumns }?: ListColumnOptions) => readonly ListColumn[]
173
+ export interface ListFilterDescriptor { key: string label: string options: readonly ListFilterOption[] op: EntryFilter["op"] valueType: "string" | "boolean" }
174
+ export interface ListFilterOption { value: string label: string }
175
+ function listFilterQuery: (type: ContentTypeLike, filters: Readonly<Record<string, string>>) => readonly EntryFilter[]
176
+ function listFilters: (type: ContentTypeLike) => readonly ListFilterDescriptor[]
177
+ export interface ListQuery { filters?: readonly EntryFilter[] sort?: SortSpec search?: string limit?: number cursor?: string | null status?: EntryStatus | "any" }
178
+ export interface ListViewState { search: string status: EntryStatus | "any" filters: Readonly<Record<string, string>> sort: SortSpec cursor: string | null }
179
+ export class LockedTypeError extends Error { override readonly name = "LockedTypeError" readonly slug: string constructor(slug: string, reason?: string) { super( reason ?? `Content type "${slug}" is locked. It is defined in code and cannot be changed through the Schema Builder or the adapter.`, ) this.slug = slug } }
180
+ const MATRIX_ACTIONS: ReadonlyArray<EntryAction>
181
+ const MATRIX_LIMITS: { readonly admin: "An admin is never restricted by a matrix. Removing every other role from an action leaves it admin-only; there is no way to lock an admin out of their own project."; readonly ownership: "An author may only ever update or delete entries they created. Ticking an author here grants the action on their own entries, never on somebody else's."; readonly narrowing: "A matrix can only take permissions away. Ticking a role that its own role does not grant changes nothing, and the row says so."; readonly publicRead: "Denying read hides entries from the admin, not from the public website. A published entry is readable by anyone with its address by design; unpublish it to take it down."; }
182
+ export interface MatrixCell { role: Role listed: boolean decision: MatrixDecision fixed: boolean }
183
+ function matrixDecision: (type: Pick<ContentTypeLike, "permissions"> | undefined, role: Role, action: EntryAction) => MatrixDecision
184
+ export type MatrixDecision = Decision
185
+ export interface MatrixRow { action: EntryAction cells: readonly MatrixCell[] }
186
+ const MAX_ARRAY_ITEMS: 1000
187
+ const MAX_CONTENT_TYPE_FIELDS: 100
188
+ const MAX_ENTRY_FIELDS: 200
189
+ const MAX_ENTRY_INDEXES: 200
190
+ const MAX_FIELD_COLUMNS: 2
191
+ const MAX_FIELD_PURGE: 250
192
+ const MAX_IMAGE_BYTES: number
193
+ const MAX_MEDIA_BYTES: number
194
+ const MAX_MEDIA_REFS: 100
195
+ const MAX_NESTING_DEPTH: 20
196
+ const MAX_PAGE_SIZE: 100
197
+ const MAX_SETTINGS_FIELDS: 100
198
+ const MAX_WRITE_BYTES: 900000
199
+ const MEDIA_ACCEPT_ATTRIBUTE: string
200
+ const MEDIA_REFS_KEY: "mediaRefs"
201
+ const MEDIA_USAGE_LIMIT: 25
202
+ export interface MediaAdapter { list(query?: MediaQuery): Promise<Page<MediaItem>> getById(mediaId: string): Promise<MediaItem | null> getMany(mediaIds: readonly string[]): Promise<readonly MediaItem[]> reserveId(): string create(input: MediaInput, mediaId?: string): Promise<MediaItem> update(mediaId: string, patch: Partial<MediaInput>): Promise<MediaItem> delete(mediaId: string): Promise<void> usages( mediaId: string, query?: MediaUsageQuery, ): Promise<readonly MediaUsageReference[]> refreshReferences(mediaId: string, url: string): Promise<MediaRefreshResult> usage(): Promise<MediaUsage> }
203
+ export type MediaCategory = "image" | "video" | "audio" | "document"
204
+ export interface MediaFileFacts { name: string type: string size: number }
205
+ export interface MediaInput { name: string type: string size: number url: string path: string alt?: string | null width?: number | null height?: number | null }
206
+ export interface MediaItem { id: string name: string type: string size: number url: string path: string alt: string | null width: number | null height: number | null createdAt: string createdBy: string }
207
+ export interface MediaQuery { limit?: number cursor?: string | null type?: string }
208
+ export interface MediaReader { getById(mediaId: string): Promise<MediaItem | null> getMany(mediaIds: readonly string[]): Promise<readonly MediaItem[]> }
209
+ export interface MediaReference { mediaId: string url: string }
210
+ export interface MediaRefreshResult { updated: number failed: readonly MediaUsageReference[] }
211
+ export interface MediaRejection { code: MediaRejectionCode message: string }
212
+ export type MediaRejectionCode = "type-not-allowed" | "too-large" | "empty" | "extension-mismatch"
213
+ function mediaTag: (mediaId: string) => string
214
+ function mediaTypeRule: (type: string) => MediaTypeRule | null
215
+ export interface MediaTypeRule { readonly type: string readonly category: MediaCategory readonly maxBytes: number readonly extensions: readonly string[] }
216
+ export interface MediaUsage { count: number bytes: number }
217
+ export interface MediaUsageQuery { limit?: number }
218
+ export interface MediaUsageReference { contentType: string entryId: string title: string | null }
219
+ function mergeIssues: (client: readonly FieldError[], server: readonly FieldError[]) => readonly FormIssue[]
220
+ const MIGRATION_BATCH_SIZE: 50
221
+ const MIGRATION_MAX_BATCHES: 20
222
+ export interface MigrationAdapter { countBehind(slug: string, targetVersion: number): Promise<number> listBehind( slug: string, targetVersion: number, query?: { limit?: number }, ): Promise<Page<StoredEntryRecord>> applyMigration( slug: string, entryId: string, values: Record<string, unknown>, write: MigrationWrite, ): Promise<boolean> }
223
+ export interface MigrationContext { entryId: string slug: string fromVersion: number }
224
+ export interface MigrationFailure { entryId: string fromVersion: number reason: string }
225
+ export interface MigrationRegistry { forType(slug: string): readonly EntryMigration[] plan(type: ContentTypeLike, fromVersion: number): readonly EntryMigration[] }
226
+ export interface MigrationRunDeps { migrations: MigrationAdapter registry: MigrationRegistry type: ContentTypeLike }
227
+ export interface MigrationRunOptions { batchSize?: number maxBatches?: number onProgress?: (progress: { scanned: number; migrated: number }) => void }
228
+ export interface MigrationRunResult { slug: string targetVersion: number scanned: number migrated: number skipped: number failed: readonly MigrationFailure[] remaining: boolean }
229
+ export interface MigrationWrite { expectedVersion: number nextVersion: number }
230
+ const MIN_WRITE_INTERVAL_MS: 200
231
+ function missingEntryIndexes: (file: { indexes?: readonly FirestoreIndex[]; }, types: readonly ContentTypeLike[]) => readonly FirestoreIndex[]
232
+ function missingError: (subject: string) => ClassifiedError
233
+ function moveFieldRow: (rows: readonly FieldRow[], from: number, to: number) => FieldRow[]
234
+ function moveItem: <TItem>(items: readonly TItem[], from: number, to: number) => TItem[]
235
+ export interface NestedConverter { toStored(fields: FieldMap, values: Record<string, unknown>): Record<string, unknown> fromStored(fields: FieldMap, data: Record<string, unknown>): Record<string, unknown> }
236
+ function newContentType: (input: NewContentTypeInput) => ContentTypeLike
237
+ export interface NewContentTypeInput { name: string slug: string kind: ContentTypeKind icon?: string namePlural?: string description?: string }
238
+ function newFieldDefinition: (registry: FieldTypeRegistry, type: FieldType, label: string) => FieldDefinition
239
+ function nextSchemaVersion: (previous: ContentTypeLike | null, next: ContentTypeLike) => number
240
+ function nextSort: (current: SortSpec, column: ListColumn, key?: string) => SortSpec
241
+ function normalizeConfirmation: (value: string) => string
242
+ function normalizeLimit: (limit: number | undefined) => number
243
+ function normalizeMediaType: (type: string) => string
244
+ function normalizePermissionMatrix: (matrix: PermissionMatrix | undefined) => PermissionMatrix | undefined
245
+ export interface NumberField extends FieldCommon { type: "number" step?: number defaultValue?: number }
246
+ function orderedFieldKeys: (type: Pick<ContentTypeLike, "fields" | "fieldOrder">) => readonly string[]
247
+ function orderedFields: (type: Pick<ContentTypeLike, "fields" | "fieldOrder">) => readonly FieldRow[]
248
+ export interface Page<TItem> { items: readonly TItem[] cursor: string | null hasMore: boolean }
249
+ const PATTERN_PRESETS: { readonly email: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"; readonly slug: "^[a-z0-9]+(?:-[a-z0-9]+)*$"; readonly url: "^https?://[^\\s]+$"; }
250
+ export interface PayloadLimits { maxFields?: number maxBytes?: number }
251
+ export interface Permission { action: Action contentType?: string }
252
+ const PERMISSION_DEFAULTS: Readonly<Record<"admin" | "editor" | "author", Readonly<Record<Action, Decision>>>>
253
+ export interface PermissionContext { type?: ContentTypeLike entry?: EntryOwnership }
254
+ export class PermissionError extends Error { override readonly name = "PermissionError" readonly action: string readonly contentType: string | undefined constructor(action: string, contentType?: string, message?: string) { super( message ?? (contentType === undefined ? `This account may not ${action}.` : `This account may not ${action} entries of "${contentType}".`), ) this.action = action this.contentType = contentType } }
255
+ export type PermissionMatrix = Partial<Record<EntryAction, readonly Role[]>>
256
+ function permissionMatrixRows: (type: Pick<ContentTypeLike, "permissions"> | undefined) => readonly MatrixRow[]
257
+ function registerFieldTypes: (specs: readonly AnyFieldTypeSpec[]) => FieldTypeRegistry
258
+ export interface Registry { readonly types: readonly RegistryEntry[] readonly conflicts: readonly RegistryEntry[] readonly invalid: readonly InvalidContentType[] get(slug: string): RegistryEntry | undefined require(slug: string): RegistryEntry has(slug: string): boolean bySource(source: ContentTypeSource): readonly RegistryEntry[] }
259
+ export interface RegistryEntry { type: ContentTypeLike source: ContentTypeSource locked: boolean conflict?: { source: "database"; type: ContentTypeLike } }
260
+ export interface RegistryStore { readonly registry: Registry setDatabaseTypes(types: readonly ContentTypeLike[]): boolean subscribe(listener: (registry: Registry) => void): () => void readonly revision: number }
261
+ export interface RelationField<TTo extends string = string> extends FieldCommon { type: "relation" to: TTo many?: boolean }
262
+ export interface RemovalContext { removedBy: string removedAt?: string }
263
+ export interface RemovedField { field: FieldDefinition removedAt: string removedBy: string }
264
+ function removedFieldEntries: (type: ContentTypeLike) => readonly (RemovedField & { key: string; })[]
265
+ export type RemovedFieldMap = { readonly [key: string]: RemovedField }
266
+ function removeField: (type: ContentTypeLike, key: string, context: RemovalContext) => ContentTypeLike
267
+ export interface RepeaterField<TFields extends FieldMap = FieldMap> extends FieldCommon { type: "repeater" fields: TFields min?: number max?: number }
268
+ function requiredEntryIndexes: (types: readonly ContentTypeLike[]) => readonly FirestoreIndex[]
269
+ const RESERVED_FIELD_KEYS: readonly ["id", "status", "createdAt", "createdBy", "updatedAt", "updatedBy", "publishedAt", "schemaVersion"]
270
+ const RESERVED_TYPE_SLUGS: ReadonlyArray<string>
271
+ export type ReservedFieldKey = (typeof RESERVED_FIELD_KEYS)[number]
272
+ function resolveDecision: (role: Role, action: Action, type: Pick<ContentTypeLike, "permissions"> | undefined) => Decision
273
+ function resolveTsType: (registry: FieldTypeRegistry, field: FieldDefinition) => string
274
+ function restoreField: (type: ContentTypeLike, key: string, options?: RestoreOptions) => ContentTypeLike
275
+ export interface RestoreOptions { field?: FieldDefinition }
276
+ function retargetMediaUrls: (data: Record<string, unknown>, mediaId: string, url: string) => Record<string, unknown> | null
277
+ export interface RichtextField extends FieldCommon { type: "richtext" features?: readonly string[] }
278
+ export interface RichTextValue { format: "husk-richtext-v1" content: unknown }
279
+ export type Role = (typeof ROLES)[number]
280
+ const ROLES: readonly ["admin", "editor", "author"]
281
+ function runMigration: (deps: MigrationRunDeps, options?: MigrationRunOptions) => Promise<MigrationRunResult>
282
+ function safeFileName: (name: string, type: string) => string
283
+ function sameDefinition: (left: ContentTypeLike, right: ContentTypeLike) => boolean
284
+ export interface SchemaAdapter { list(): Promise<readonly ContentTypeLike[]> get(slug: string): Promise<ContentTypeLike | null> create(type: ContentTypeLike): Promise<ContentTypeLike> update(slug: string, type: ContentTypeLike): Promise<ContentTypeLike> delete(slug: string): Promise<void> watch( listener: (types: readonly ContentTypeLike[]) => void, onError?: (error: Error) => void, ): () => void }
285
+ export class SchemaError extends Error { override readonly name = "SchemaError" constructor(message: string) { super(message) } }
286
+ function schemaVersionOf: (type: Pick<ContentTypeLike, "schemaVersion">) => number
287
+ export interface SelectField<TValue extends string = string> extends FieldCommon { type: "select" options: readonly SelectOption<TValue>[] multiple?: boolean defaultValue?: TValue }
288
+ export interface SelectOption<TValue extends string = string> { value: TValue label: string }
289
+ export interface SettingsAdapter { get(key: string): Promise<Record<string, unknown> | null> set(key: string, value: Record<string, unknown>): Promise<void> }
290
+ export interface SettingsReader { get(key: string): Promise<Record<string, unknown> | null> }
291
+ function settingsTag: (key: string) => string
292
+ export interface SingleReader<TEntry = Entry> { get(): Promise<TEntry | null> }
293
+ function singletonEntryId: (slug: string) => string
294
+ export class SingletonError extends Error { override readonly name = "SingletonError" readonly slug: string constructor(slug: string, message: string) { super(message) this.slug = slug } }
295
+ export interface SlugAvailability { available: boolean reason?: SlugRejection message?: string source?: ContentTypeSource }
296
+ export interface SlugField extends FieldCommon { type: "slug" from?: string }
297
+ function slugFieldKey: (type: ContentTypeLike) => string | null
298
+ export type SlugOf<TTypes extends ContentTypeMap> = KnownSlug<TTypes> | (string & Record<never, never>)
299
+ export type SlugRejection = "invalid" | "reserved" | "taken-by-code" | "taken-by-database"
300
+ function sortableFieldKeys: (type: ContentTypeLike) => readonly string[]
301
+ export interface SortSpec { field: string direction: "asc" | "desc" }
302
+ function staleWriteError: (subject: string) => ClassifiedError
303
+ const STATUS_EFFECT: Readonly<Record<"draft" | "published" | "archived", string>>
304
+ export type StatusSubject = Pick<EntryMeta, "status" | "createdBy">
305
+ export interface StatusTransition { to: EntryStatus label: string effect: string allowed: boolean reason: string | null }
306
+ function statusTransitions: (user: AuthUser, type: ContentTypeLike, entry: StatusSubject | null) => readonly StatusTransition[]
307
+ function storageShapeChanged: (previous: ContentTypeLike, next: ContentTypeLike) => boolean
308
+ function storageSignature: (type: Pick<ContentTypeLike, "kind" | "fields">) => string
309
+ function storageSignatureOf: (field: FieldDefinition) => string
310
+ export interface StoredEntryRecord { id: string schemaVersion: number values: Record<string, unknown> }
311
+ function storesTheSameWay: (left: FieldDefinition, right: FieldDefinition) => boolean
312
+ function suggestFieldKey: (label: string) => string
313
+ export interface TextareaField extends FieldCommon { type: "textarea" rows?: number defaultValue?: string }
314
+ export interface TextField extends FieldCommon { type: "text" defaultValue?: string }
315
+ function treatmentFor: (category: ErrorCategory) => ErrorTreatment
316
+ export type TsTypeResolver = (field: FieldDefinition) => string
317
+ function uniqueFieldPaths: (type: AnyContentTypeShape) => string[]
318
+ const UNKNOWN_TS_TYPE: "unknown"
319
+ function unroutableIssues: <TError extends FieldError>(issues: readonly TError[], type: ContentTypeLike) => readonly TError[]
320
+ const UNTITLED_ENTRY: "Untitled"
321
+ export interface ValidateCtx { path: string contentType: AnyContentTypeShape entry: Record<string, unknown> }
322
+ function validateEntry: (type: AnyContentTypeShape, values: Record<string, unknown>, options?: ValidateOptions) => FieldError[]
323
+ export interface ValidateOptions { skipRequired?: boolean fieldTypes?: FieldTypeRegistry }
324
+ const VALIDATION_RULE_CONTROLS: { readonly required: ValidationRuleControl; readonly minLength: ValidationRuleControl; readonly maxLength: ValidationRuleControl; readonly min: ValidationRuleControl; readonly max: ValidationRuleControl; readonly pattern: ValidationRuleControl; readonly unique: ValidationRuleControl; }
325
+ export class ValidationError extends Error { override readonly name = "ValidationError" readonly errors: readonly FieldError[] constructor(errors: readonly FieldError[]) { super( errors.length === 1 && errors[0] ? `${errors[0].path}: ${errors[0].message}` : `${errors.length} fields failed validation`, ) this.errors = errors } }
326
+ export interface ValidationRuleControl { label: string control: "boolean" | "number" | "text" help: string }
327
+ export type ValidationRuleName = keyof ValidationRules
328
+ export interface ValidationRules { required?: boolean minLength?: number maxLength?: number min?: number max?: number pattern?: string unique?: boolean }
329
+ function valuesChanged: (baseline: Record<string, unknown>, current: Record<string, unknown>) => boolean
330
+ function withFieldOption: (field: FieldDefinition, option: string, value: unknown) => FieldDefinition
331
+ function withFieldRows: (type: ContentTypeLike, rows: readonly FieldRow[]) => ContentTypeLike
332
+ function withMatrixRole: (matrix: PermissionMatrix | undefined, action: EntryAction, role: Role, listed: boolean) => PermissionMatrix | undefined
333
+ function withValidationRule: (field: FieldDefinition, rule: ValidationRuleName, value: string | number | boolean | undefined) => FieldDefinition
334
+ const WRITE_BURST_LIMIT: 200
335
+ const WRITE_BURST_WINDOW_MS: 10000
336
+ export type WriteLimitCode = "too-many-fields" | "too-large" | "too-deep" | "too-many-items" | "too-fast"
337
+ export class WriteLimitError extends Error { override readonly name = "WriteLimitError" readonly code: string readonly path: string constructor(rejection: { code: string; path: string; message: string }) { super(rejection.message) this.code = rejection.code this.path = rejection.path } }
338
+ export interface WriteLimitRejection { code: WriteLimitCode path: string message: string }
339
+ export interface WriteRateLimiter { check(): WriteLimitRejection | null }
340
+ ```
341
+
342
+ ## `@nonext/husk/admin`
343
+
344
+ 35 exports.
345
+
346
+ ```ts
347
+ const ADMIN_TITLE: "Husk"
348
+ export type AdminAccess = | { outcome: "allow"; user: AuthUser } | { outcome: "sign-in"; location: string; reason: "required" | "expired" } | { outcome: "unauthorized"; user: AuthUser; action: Action }
349
+ export interface AdminNavGroup { key: string label?: string items: readonly AdminNavItem[] }
350
+ export interface AdminNavItem { key: string label: string href: string icon: IconName exact?: boolean }
351
+ function adminPageTitle: (navigation: readonly AdminNavGroup[], pathname: string) => string | undefined
352
+ function buildAdminNavigation: ({ registry, user, basePath, }: BuildNavigationOptions) => readonly AdminNavGroup[]
353
+ export interface BuildNavigationOptions { registry: Registry user: AuthUser basePath?: string }
354
+ function clearedSessionCookie: (options: { secure: boolean; }) => string
355
+ function contentTypeIcon: (type: ContentTypeLike) => IconName
356
+ function contentTypeLabel: (type: ContentTypeLike) => string
357
+ export interface CookieReader { get(name: string): { value: string } | undefined }
358
+ function createEmulatorTokenVerifier: (options: EmulatorTokenVerifierOptions) => TokenVerifier
359
+ function createSessionRoutes: (options: SessionRouteOptions) => SessionRoutes
360
+ function createUnavailableTokenVerifier: () => TokenVerifier
361
+ function decodeIdTokenPayload: (token: string) => Record<string, unknown>
362
+ const DEFAULT_LOGIN_PATH: "/admin/login"
363
+ export interface EmulatorTokenVerifierOptions { projectId: string now?: () => number }
364
+ function isNavItemActive: (item: AdminNavItem, pathname: string) => boolean
365
+ function navItemForPath: (navigation: readonly AdminNavGroup[], pathname: string) => AdminNavItem | undefined
366
+ function readSession: (cookies: CookieReader, verifier: TokenVerifier, cookieName: string) => Promise<SessionState>
367
+ function readSessionCookie: (header: string | null) => string | undefined
368
+ export interface ResolveAccessOptions { session: SessionState pathname: string action?: Action context?: PermissionContext loginPath?: string }
369
+ function resolveAdminAccess: (options: ResolveAccessOptions) => AdminAccess
370
+ function safeReturnPath: (next: string | null | undefined, adminBasePath?: string) => string
371
+ function serializeSessionCookie: (token: string, options: SessionCookieOptions) => string
372
+ const SESSION_COOKIE: "husk_session"
373
+ export interface SessionClaims { uid: string email: string | null displayName: string | null role: Role disabled: boolean expiresAt: number }
374
+ export interface SessionCookieOptions { maxAge: number secure: boolean }
375
+ export interface SessionRouteOptions { verifier: TokenVerifier secure: boolean now?: () => number }
376
+ export interface SessionRoutes { POST(request: Request): Promise<Response> DELETE(): Promise<Response> }
377
+ export type SessionState = | { status: "authenticated"; user: AuthUser } | { status: "anonymous" } | { status: "expired" } | { status: "invalid"; reason: string }
378
+ export type TokenErrorCode = "malformed" | "expired" | "untrusted"
379
+ export class TokenVerificationError extends Error { override readonly name = "TokenVerificationError" readonly code: TokenErrorCode constructor(code: TokenErrorCode, message: string) { super(message) this.code = code } }
380
+ export interface TokenVerifier { verify(token: string): Promise<SessionClaims> }
381
+ function toSessionUser: (claims: SessionClaims) => AuthUser
382
+ ```
383
+
384
+ ## `@nonext/husk/admin/ui`
385
+
386
+ 208 exports.
387
+
388
+ ```ts
389
+ function AdminMediaPickerProvider: ({ adapter, user, uploader, contentTypeLabel, children, }: AdminMediaPickerProviderProps) => JSX.Element
390
+ export interface AdminMediaPickerProviderProps { adapter: CmsAdapter user: AuthUser uploader?: MediaUploader | undefined contentTypeLabel?: ((slug: string) => string) | undefined children: ReactNode }
391
+ function AdminShell: ({ navigation, user, accountMenu, children }: AdminShellProps) => JSX.Element
392
+ export interface AdminShellProps { navigation: readonly AdminNavGroup[] user: AuthUser accountMenu?: ReactNode children: ReactNode }
393
+ function AdminSidebar: ({ navigation, pathname, user, accountMenu, className, panelRef, onNavigate, onKeyDown, }: AdminSidebarProps) => JSX.Element
394
+ export interface AdminSidebarProps { navigation: readonly AdminNavGroup[] pathname: string user: AuthUser accountMenu?: ReactNode className?: string panelRef?: Ref<HTMLElement> onNavigate?: () => void onKeyDown?: (event: KeyboardEvent<HTMLElement>) => void }
395
+ export interface AuthorInfo { uid: string displayName?: string | null email?: string | null }
396
+ export type AuthorLookup = (uid: string) => Promise<AuthorInfo | null>
397
+ function Avatar: ({ name, src, size }: AvatarProps) => JSX.Element
398
+ export interface AvatarProps { name: string src?: string size?: AvatarSize }
399
+ export type AvatarSize = 20 | 24 | 32
400
+ function Badge: ({ children, variant, icon }: BadgeProps) => JSX.Element
401
+ export interface BadgeProps { children: ReactNode variant?: BadgeVariant icon?: IconName }
402
+ export type BadgeVariant = "neutral" | "success" | "warning" | "danger" | "info" | "accent"
403
+ function Breadcrumb: ({ items, className }: BreadcrumbProps) => JSX.Element | null
404
+ export interface BreadcrumbItem { label: string href?: string }
405
+ export interface BreadcrumbProps { items: readonly BreadcrumbItem[] className?: string }
406
+ function Button: ({ variant, size, loading, icon, children, className, disabled, type, ref, ...rest }: ButtonProps) => JSX.Element
407
+ export interface ButtonProps extends Omit< ButtonHTMLAttributes<HTMLButtonElement>, "type" > { variant?: ButtonVariant size?: ButtonSize loading?: boolean icon?: IconName type?: "button" | "submit" | "reset" ref?: Ref<HTMLButtonElement> }
408
+ export type ButtonSize = "sm" | "md" | "lg"
409
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger"
410
+ function canDeleteMedia: (user: AuthUser) => boolean
411
+ function canReplaceMedia: (user: AuthUser, item: MediaItem) => boolean
412
+ function Checkbox: ({ checked, onChange, label, ariaLabel, name, value, disabled, id, "aria-describedby": describedBy, }: CheckboxProps) => JSX.Element
413
+ export interface CheckboxProps { checked: boolean | "indeterminate" onChange: (checked: boolean) => void label?: string ariaLabel?: string name?: string value?: string disabled?: boolean id?: string "aria-describedby"?: string | undefined }
414
+ export type ClipboardOutcome = "copied" | "unavailable"
415
+ function CollectionList: ({ adapter, type, user, onOpen, onCreate, onDelete, refreshToken, pageSize, }: CollectionListProps) => JSX.Element
416
+ export interface CollectionListProps { adapter: CmsAdapter type: ContentTypeLike user: AuthUser onOpen?: (entryId: string) => void onCreate?: () => void onDelete?: (entryIds: readonly string[]) => void refreshToken?: unknown pageSize?: number }
417
+ function ContentTypeEditor: ({ adapter, registry, entry, user, backHref, }: ContentTypeEditorProps) => JSX.Element
418
+ export interface ContentTypeEditorProps { adapter: CmsAdapter registry: Registry entry: RegistryEntry user: AuthUser backHref?: string }
419
+ function ContentTypesScreen: ({ registry, adapter, user, onOpen, onCreated, liveError, }: ContentTypesScreenProps) => JSX.Element
420
+ export interface ContentTypesScreenProps { registry: Registry adapter: CmsAdapter user: AuthUser onOpen: (slug: string) => void onCreated?: (slug: string) => void liveError?: Error | null }
421
+ function copyText: (text: string) => Promise<ClipboardOutcome>
422
+ function CreateContentTypeDialog: (props: CreateContentTypeDialogProps) => JSX.Element | null
423
+ export interface CreateContentTypeDialogProps { open: boolean registry: Registry adapter: CmsAdapter onClose: () => void onCreated: (slug: string) => void }
424
+ function Dashboard: ({ adapter, types, recentLimit }: DashboardProps) => JSX.Element
425
+ export interface DashboardCount { slug: string label: string kind: ContentTypeKind icon: IconName count: number }
426
+ export interface DashboardData { counts: readonly DashboardCount[] recent: readonly DashboardRecentEntry[] media: MediaUsage | null }
427
+ export interface DashboardProps { adapter: CmsAdapter types: readonly ContentTypeLike[] recentLimit?: number }
428
+ export interface DashboardRecentEntry { key: string entryId: string title: string typeSlug: string typeLabel: string status: EntryStatus updatedAt: string }
429
+ const DEFAULT_RECENT_LIMIT: 8
430
+ function DeleteEntriesDialog: ({ adapter, type, entryIds, onCancel, onDeleted, role, }: DeleteEntriesDialogProps) => JSX.Element | null
431
+ export interface DeleteEntriesDialogProps { adapter: CmsAdapter type: ContentTypeLike entryIds: readonly string[] onCancel: () => void onDeleted: (entryIds: readonly string[], complete: boolean) => void role?: Role | undefined }
432
+ function describeUsage: (usages: readonly MediaUsageReference[], limit?: number) => string
433
+ function EmptyState: ({ icon, heading, description, action }: EmptyStateProps) => JSX.Element
434
+ export interface EmptyStateProps { icon?: IconName heading: string description?: string action?: ReactNode }
435
+ const ENTRY_STATUS_LABEL: { readonly draft: "Draft"; readonly published: "Published"; readonly archived: "Archived"; }
436
+ const ENTRY_STATUS_VARIANT: { readonly draft: "neutral"; readonly published: "success"; readonly archived: "warning"; readonly error: "danger"; }
437
+ function EntryEditor: ({ adapter, type, user, entryId, onSaved, onCancel, onDirtyChange, onDelete, resolveAuthor, getType, registry, }: EntryEditorProps) => JSX.Element
438
+ export interface EntryEditorProps { adapter: CmsAdapter type: ContentTypeLike user: AuthUser entryId?: string | undefined onSaved?: ((entry: Entry, created: boolean) => void) | undefined onCancel?: (() => void) | undefined onDirtyChange?: ((dirty: boolean) => void) | undefined onDelete?: ((entryId: string) => void) | undefined resolveAuthor?: AuthorLookup | undefined getType?: ((slug: string) => ContentTypeLike | undefined) | undefined registry?: FieldTypeRegistry | undefined }
439
+ function EntryMetadata: ({ user, entry, status, pending, transitions, onTransition, busy, resolveAuthor, actions, }: EntryMetadataProps) => JSX.Element
440
+ export interface EntryMetadataProps { user: AuthUser entry: Entry | null status: EntryStatus pending?: EntryStatus | null transitions: readonly StatusTransition[] onTransition: (to: EntryStatus) => void busy?: boolean resolveAuthor?: AuthorLookup | undefined actions?: ReactNode }
441
+ function ErrorCapture: ({ onCapture }: ErrorCaptureProps) => null
442
+ export interface ErrorCaptureProps { onCapture?: ((error: ClassifiedError) => void) | undefined }
443
+ function ErrorNotice: ({ error, onRetry, actions, className }: ErrorNoticeProps) => JSX.Element
444
+ export interface ErrorNoticeProps { error: ClassifiedError onRetry?: (() => void) | undefined actions?: ReactNode className?: string }
445
+ function ErrorScreen: ({ error, title, onRetry, children }: ErrorScreenProps) => JSX.Element
446
+ export interface ErrorScreenProps { error: ClassifiedError title?: string | undefined onRetry?: (() => void) | undefined children?: ReactNode }
447
+ function Field: ({ label, required, optional, help, error, children, }: FieldProps) => JSX.Element
448
+ export interface FieldFormContextValue { values: Record<string, unknown> fields?: FieldMap | undefined checkUnique?: UniqueCheck | undefined }
449
+ function FieldFormProvider: ({ value, children }: FieldFormProviderProps) => JSX.Element
450
+ export interface FieldFormProviderProps { value: FieldFormContextValue children: ReactNode }
451
+ export interface FieldProps { label: string required?: boolean optional?: boolean help?: string | undefined error?: string | undefined children: (control: { id: string describedBy: string | undefined invalid: boolean }) => ReactNode }
452
+ function FieldRows: ({ rows, registry, contentTypes, readOnly, reorderable, checkKey, onChange, onRemove, label, issues, }: FieldRowsProps) => JSX.Element
453
+ export interface FieldRowsProps { rows: readonly FieldRow[] registry: FieldTypeRegistry contentTypes: readonly SelectOption[] readOnly: boolean reorderable?: boolean checkKey: (key: string) => FieldKeyAvailability onChange: (rows: readonly FieldRow[]) => void onRemove?: (key: string) => void label: string issues?: readonly (FieldOptionIssue & { key: string })[] }
454
+ function FieldShell: ({ field, errors, help, children }: FieldShellProps) => JSX.Element
455
+ export interface FieldShellProps { field: FieldDefinition errors: readonly FieldError[] help?: string | undefined children: FieldProps["children"] }
456
+ const fieldTypes: FieldTypeRegistry
457
+ function formatBytes: (bytes: number) => string
458
+ function formatCellValue: (value: unknown, align: ColumnAlign) => string | null
459
+ function formatDate: (value: string) => string
460
+ function formatDateTime: (iso: string) => string
461
+ function formatDimensions: (item: MediaItem) => string | null
462
+ function FormError: ({ children }: { children: ReactNode; }) => JSX.Element
463
+ function Icon: ({ name, size, label, className }: IconProps) => JSX.Element
464
+ function IconButton: (props: IconButtonProps) => JSX.Element
465
+ export type IconButtonProps = IconButtonBase & ({ icon: IconName; morph?: false } | { icon: MorphIconName; morph: true })
466
+ export type IconName = (typeof ICON_NAMES)[number]
467
+ export interface IconProps { name: IconName size?: IconSize label?: string className?: string }
468
+ const ICONS: { readonly brand: LucideIcon; readonly create: LucideIcon; readonly edit: LucideIcon; readonly delete: LucideIcon; readonly save: LucideIcon; readonly close: LucideIcon; readonly search: LucideIcon; readonly filter: LucideIcon; readonly "sort-asc": LucideIcon; readonly "sort-desc": LucideIcon; readonly "navigate-into": LucideIcon; readonly "page-previous": LucideIcon; readonly "page-next": LucideIcon; readonly expand: LucideIcon; readonly collapse: LucideIcon; readonly selected: LucideIcon; readonly indeterminate: LucideIcon; readonly "more-actions": LucideIcon; readonly "drag-handle": LucideIcon; readonly locked: LucideIcon; readonly dashboard: LucideIcon; readonly collection: LucideIcon; readonly singleton: LucideIcon; readonly media: LucideIcon; readonly settings: LucideIcon; readonly users: LucideIcon; readonly permissions: LucideIcon; readonly "content-types": LucideIcon; readonly system: LucideIcon; readonly success: LucideIcon; readonly warning: LucideIcon; readonly error: LucideIcon; readonly info: LucideIcon; readonly "external-link": LucideIcon; readonly upload: LucideIcon; readonly copy: LucideIcon; readonly retry: LucideIcon; readonly "open-navigation": LucideIcon; readonly bold: LucideIcon; readonly italic: LucideIcon; readonly strikethrough: LucideIcon; readonly link: LucideIcon; readonly unlink: LucideIcon; readonly "bullet-list": LucideIcon; readonly "numbered-list": LucideIcon; readonly quote: LucideIcon; readonly undo: LucideIcon; readonly redo: LucideIcon; }
469
+ export type IconSize = 12 | 14 | 16 | 18 | 20 | 24
470
+ function initials: (name: string) => string
471
+ function isDeletableType: (type: ContentTypeLike) => boolean
472
+ function isImageItem: (item: MediaItem) => boolean
473
+ export interface LiveRegistry { registry: Registry synced: boolean error: Error | null }
474
+ function loadDashboard: (adapter: CmsAdapter, types: readonly ContentTypeLike[], { recentLimit }?: LoadDashboardOptions) => Promise<DashboardData>
475
+ export interface LoadDashboardOptions { recentLimit?: number }
476
+ export interface LoginCredentials { email: string password: string }
477
+ function LoginScreen: ({ onSignIn, expired }: LoginScreenProps) => JSX.Element
478
+ export interface LoginScreenProps { onSignIn: (credentials: LoginCredentials) => Promise<void> expired?: boolean }
479
+ const MEDIA_FILTERS: ReadonlyArray<{ value: string; label: string; }>
480
+ function MediaDeleteDialog: ({ adapter, item, onCancel, onDeleted, contentTypeLabel, }: MediaDeleteDialogProps) => JSX.Element | null
481
+ export interface MediaDeleteDialogProps { adapter: CmsAdapter item: MediaItem | null onCancel: () => void onDeleted: (mediaId: string) => void contentTypeLabel?: ((slug: string) => string) | undefined }
482
+ function MediaDetail: ({ adapter, user, item, onClose, onUpdated, onDelete, onReplace, contentTypeLabel, }: MediaDetailProps) => JSX.Element
483
+ export interface MediaDetailProps { adapter: CmsAdapter user: AuthUser item: MediaItem onClose: () => void onUpdated: (item: MediaItem) => void onDelete?: ((item: MediaItem) => void) | undefined onReplace?: ((item: MediaItem) => void) | undefined contentTypeLabel?: ((slug: string) => string) | undefined }
484
+ function MediaLibrary: ({ adapter, user, uploader, accept, onPick, contentTypeLabel, }: MediaLibraryProps) => JSX.Element
485
+ export interface MediaLibraryProps { adapter: CmsAdapter user: AuthUser uploader?: MediaUploader | undefined accept?: readonly string[] | undefined onPick?: ((item: MediaItem) => void) | undefined contentTypeLabel?: ((slug: string) => string) | undefined }
486
+ export type MediaLookup = | { kind: "empty" } | { kind: "unavailable" } | { kind: "loading" } | { kind: "found"; item: MediaItem } | { kind: "missing" }
487
+ export interface MediaPickerApi { resolve(mediaIds: readonly string[]): Promise<readonly MediaItem[]> pick(request: MediaPickRequest): Promise<MediaItem | null> }
488
+ function MediaPickerProvider: ({ value, children }: MediaPickerProviderProps) => JSX.Element
489
+ export interface MediaPickerProviderProps { value: MediaPickerApi children: ReactNode }
490
+ export interface MediaPickRequest { accept?: readonly string[] | undefined }
491
+ function MediaTile: ({ item, selected, unused, onOpen, }: MediaTileProps) => JSX.Element
492
+ export interface MediaTileProps { item: MediaItem selected?: boolean unused?: boolean onOpen: (item: MediaItem) => void }
493
+ export interface MediaUploader { create(file: File, options?: MediaUploadRequest): MediaUploadHandle replace( existing: MediaItem, file: File, options?: MediaUploadRequest, ): MediaUploadHandle }
494
+ export interface MediaUploadHandle { readonly mediaId: string readonly path: string readonly fileName: string progress(): MediaUploadProgress start(): Promise<MediaItem> retry(): Promise<MediaItem> cancel(): void }
495
+ export type MediaUploadPhase = "idle" | "uploading" | "storing" | "done" | "cancelled" | "failed"
496
+ export interface MediaUploadProgress { phase: MediaUploadPhase bytesTransferred: number totalBytes: number fraction: number attempt: number }
497
+ export interface MediaUploadRequest { onProgress?: (progress: MediaUploadProgress) => void alt?: string | null }
498
+ function Menu: ({ items, onSelect, label, triggerLabel, triggerIcon, triggerVariant, triggerSize, align, side, }: MenuProps) => JSX.Element
499
+ export type MenuEntry = | { kind: "item" id: string label: string icon?: IconName selected?: boolean destructive?: boolean disabled?: boolean shortcut?: string } | { kind: "separator"; id: string } | { kind: "section"; id: string; label: string }
500
+ export interface MenuProps { items: readonly MenuEntry[] onSelect: (id: string) => void label: string triggerLabel?: string triggerIcon?: IconName triggerVariant?: ButtonVariant triggerSize?: ButtonSize align?: "start" | "end" side?: "top" | "bottom" }
501
+ function Metric: ({ label, value, caption, icon, loading, unavailable, className, }: MetricProps) => JSX.Element
502
+ export interface MetricProps { label: string value?: string | number caption?: string icon?: IconName loading?: boolean unavailable?: string className?: string }
503
+ function Modal: ({ open, onClose, title, description, width, dismissible, footer, children, }: ModalProps) => ReactPortal | null
504
+ export interface ModalProps { open: boolean onClose: () => void title: string description?: string width?: ModalWidth dismissible?: boolean footer?: ReactNode children?: ReactNode }
505
+ export type ModalWidth = "sm" | "md" | "lg" | "xl"
506
+ export type MorphIconName = keyof typeof MORPH_GLYPHS
507
+ function MorphingIcon: ({ name, size, label, className }: MorphingIconProps) => JSX.Element
508
+ export interface MorphingIconProps { name: MorphIconName size?: 12 | 14 | 16 | 18 | 20 | 24 label?: string className?: string }
509
+ function NotFoundScreen: ({ children }: NotFoundScreenProps) => JSX.Element
510
+ export interface NotFoundScreenProps { children?: ReactNode }
511
+ function PageHeader: ({ title, description, breadcrumb, actions, className, }: PageHeaderProps) => JSX.Element
512
+ export interface PageHeaderProps { title: string description?: string breadcrumb?: readonly BreadcrumbItem[] actions?: ReactNode className?: string }
513
+ function PageSections: ({ children, className, }: { children: ReactNode; className?: string; }) => JSX.Element
514
+ function Pagination: ({ from, to, total, hasPrevious, hasNext, onPrevious, onNext, label, }: PaginationProps) => JSX.Element
515
+ export interface PaginationProps { from: number to: number total?: number | undefined hasPrevious: boolean hasNext: boolean onPrevious: () => void onNext: () => void label: string }
516
+ function Panel: ({ children, padding, raised, className, }: PanelProps) => JSX.Element
517
+ export type PanelPadding = "none" | "compact" | "standard" | "large"
518
+ export interface PanelProps { children: ReactNode padding?: PanelPadding raised?: boolean className?: string }
519
+ function PermissionMatrixEditor: ({ type, readOnly, onChange, }: PermissionMatrixEditorProps) => JSX.Element
520
+ export interface PermissionMatrixEditorProps { type: ContentTypeLike readOnly: boolean onChange: (next: PermissionMatrix | undefined) => void }
521
+ function pickableItems: (items: readonly MediaItem[], accept: readonly string[] | undefined) => readonly MediaItem[]
522
+ function Progress: ({ value, label, className }: ProgressProps) => JSX.Element
523
+ export interface ProgressProps { value: number label: string className?: string }
524
+ function PurgeFieldDialog: (props: PurgeFieldDialogProps) => JSX.Element | null
525
+ export interface PurgeFieldDialogProps { adapter: CmsAdapter type: ContentTypeLike fieldKey: string | null onCancel: () => void onPurged: (fieldKey: string) => void }
526
+ function queryTypeFilter: (accept: readonly string[] | undefined) => string | undefined
527
+ export interface RelationOption { id: string title: string status: EntryStatus }
528
+ export interface RelationSearchRequest { contentType: string search: string limit?: number cursor?: string | null }
529
+ export interface RelationSource { describe(contentType: string): RelationTypeInfo search(request: RelationSearchRequest): Promise<Page<RelationOption>> resolve(contentType: string, ids: readonly string[]): Promise<readonly RelationOption[]> }
530
+ function relationSourceFromAdapter: ({ adapter, getType, }: RelationSourceOptions) => RelationSource
531
+ export interface RelationSourceOptions { adapter: CmsAdapter getType: (slug: string) => ContentTypeLike | undefined }
532
+ function RelationSourceProvider: ({ value, children }: RelationSourceProviderProps) => JSX.Element
533
+ export interface RelationSourceProviderProps { value: RelationSource children: ReactNode }
534
+ export interface RelationTypeInfo { label: string searchable: boolean known: boolean }
535
+ function RemoveFieldDialog: (props: RemoveFieldDialogProps) => JSX.Element | null
536
+ export interface RemoveFieldDialogProps { adapter: CmsAdapter type: ContentTypeLike fieldKey: string | null onCancel: () => void onConfirm: (fieldKey: string) => void }
537
+ function RouteError: ({ error, reset, title, role, children }: RouteErrorProps) => JSX.Element
538
+ export interface RouteErrorProps { error: Error & { digest?: string } reset: () => void title?: string | undefined role?: Role | undefined children?: ReactNode }
539
+ export type SchemaWatch = ( listener: (types: readonly ContentTypeLike[]) => void, onError?: (error: Error) => void, ) => () => void
540
+ function SearchInput: ({ value, onChange, label, placeholder, disabled, describedBy, className, }: SearchInputProps) => JSX.Element
541
+ export interface SearchInputProps { value: string onChange: (value: string) => void label: string placeholder?: string disabled?: boolean describedBy?: string | undefined className?: string }
542
+ function Section: ({ heading, description, actions, children, className, }: SectionProps) => JSX.Element
543
+ export interface SectionProps { heading?: string description?: string actions?: ReactNode children: ReactNode className?: string }
544
+ function Select: ({ options, value, onChange, id, name, placeholder, invalid, disabled, "aria-describedby": describedBy, "aria-label": ariaLabel, }: SelectProps) => JSX.Element
545
+ export interface SelectOption { value: string label: string disabled?: boolean }
546
+ export interface SelectProps { options: readonly SelectOption[] value: string | null onChange: (value: string) => void id?: string name?: string placeholder?: string invalid?: boolean disabled?: boolean "aria-describedby"?: string | undefined "aria-label"?: string | undefined }
547
+ function Skeleton: ({ line, radius, className, label }: SkeletonProps) => JSX.Element
548
+ export type SkeletonLine = | "heading-md" | "heading-sm" | "body" | "body-sm" | "label" | "caption" | "micro" | "metric-lg" | "metric-md"
549
+ export interface SkeletonProps { line?: SkeletonLine radius?: keyof typeof RADII className?: string label?: string }
550
+ export type SortDirection = "asc" | "desc"
551
+ function Spinner: ({ size, label }: SpinnerProps) => JSX.Element
552
+ export interface SpinnerProps { size?: 14 | 16 label?: string }
553
+ function StatusDot: ({ tone, label }: StatusDotProps) => JSX.Element
554
+ export interface StatusDotProps { tone?: StatusTone label?: string }
555
+ export type StatusTone = "neutral" | "success" | "warning" | "danger" | "info"
556
+ function Switch: ({ checked, onChange, label, showLabel, disabled, id, "aria-describedby": describedBy, }: SwitchProps) => JSX.Element
557
+ export interface SwitchProps { checked: boolean onChange: (checked: boolean) => void label: string showLabel?: boolean disabled?: boolean id?: string "aria-describedby"?: string | undefined }
558
+ function Table: ({ children, label, className }: TableProps) => JSX.Element
559
+ function TableBody: ({ children }: { children: ReactNode; }) => JSX.Element
560
+ function TableCell: ({ children, primary, numeric, date, width, className, ...rest }: TableCellProps) => JSX.Element
561
+ export interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> { children?: ReactNode primary?: boolean numeric?: boolean date?: boolean width?: "selection" | "action" }
562
+ function TableEmptyRow: ({ colSpan, children, }: { colSpan: number; children: ReactNode; }) => JSX.Element
563
+ function TableHead: ({ children }: { children: ReactNode; }) => JSX.Element
564
+ function TableHeaderCell: ({ children, numeric, width, sort, className, ...rest }: TableHeaderCellProps) => JSX.Element
565
+ export interface TableHeaderCellProps extends Omit< ThHTMLAttributes<HTMLTableCellElement>, "onClick" > { children?: ReactNode numeric?: boolean width?: "selection" | "action" sort?: { direction: SortDirection | null; onSort: () => void } }
566
+ export interface TableProps { children: ReactNode label?: string className?: string }
567
+ function TableRow: ({ children, selected, disabled, onActivate, className, }: TableRowProps) => JSX.Element
568
+ export interface TableRowProps { children: ReactNode selected?: boolean disabled?: boolean onActivate?: () => void className?: string }
569
+ function Textarea: ({ invalid, className, readOnly, rows, ...rest }: TextareaProps) => JSX.Element
570
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> { invalid?: boolean }
571
+ function TextInput: ({ invalid, loading, className, readOnly, ...rest }: TextInputProps) => JSX.Element
572
+ export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> { invalid?: boolean loading?: boolean }
573
+ export interface ToastApi { notify: (options: ToastOptions) => string dismiss: (id: string) => void }
574
+ export interface ToastOptions { title: string description?: string tone?: ToastTone }
575
+ function ToastProvider: ({ children }: { children: ReactNode; }) => JSX.Element
576
+ export type ToastTone = "success" | "warning" | "error" | "info"
577
+ function Tooltip: ({ content, placement, children }: TooltipProps) => JSX.Element
578
+ export interface TooltipProps { content: string placement?: "top" | "bottom" children: ReactElement<{ "aria-describedby"?: string | undefined }> }
579
+ function UnauthorizedScreen: ({ action, role, children }: UnauthorizedScreenProps) => JSX.Element
580
+ export interface UnauthorizedScreenProps { action?: Action role?: string children?: ReactNode }
581
+ export type UniqueCheck = (path: string, value: string) => Promise<boolean>
582
+ function UnsavedChangesProvider: ({ navigate, children, }: UnsavedChangesProviderProps) => JSX.Element
583
+ export interface UnsavedChangesProviderProps { navigate?: ((href: string) => void) | undefined children: ReactNode }
584
+ function uploadAccept: (accept: readonly string[] | undefined) => string
585
+ function UsageList: ({ usages, contentTypeLabel }: UsageListProps) => JSX.Element | null
586
+ export interface UsageListProps { usages: readonly MediaUsageReference[] contentTypeLabel?: ((slug: string) => string) | undefined }
587
+ function useBeforeUnloadWarning: (dirty: boolean) => void
588
+ function useConfirmLeave: () => (proceed: () => void) => void
589
+ function useFieldForm: () => FieldFormContextValue
590
+ function useLiveRegistry: (store: RegistryStore, watch?: SchemaWatch) => LiveRegistry
591
+ function useMediaLookup: (reference: MediaReference | null) => MediaLookup
592
+ function useMediaPicker: () => MediaPickerApi | null
593
+ function useOptionalToast: () => ToastApi | null
594
+ function useRelationSource: () => RelationSource | null
595
+ function useToast: () => ToastApi
596
+ function useUnsavedChanges: (dirty: boolean) => void
597
+ ```
598
+
599
+ ## `@nonext/husk/auth`
600
+
601
+ 19 exports.
602
+
603
+ ```ts
604
+ export class AuthError extends Error { override readonly name = "AuthError" readonly code: AuthErrorCode constructor(code: AuthErrorCode, message: string) { super(message) this.code = code } }
605
+ export type AuthErrorCode = "disabled" | "invalid-credentials"
606
+ export interface AuthUser { uid: string email: string displayName: string | null role: Role disabled: boolean }
607
+ export interface ClaimSet { role: Role disabled: boolean }
608
+ export interface ClaimSync { sync(uid: string, claims: ClaimSet): Promise<void> }
609
+ export class ClaimSyncError extends Error { override readonly name = "ClaimSyncError" readonly uid: string constructor(uid: string, message: string) { super(message) this.uid = uid } }
610
+ function createEmulatorClaimSync: (options: EmulatorClaimSyncOptions) => ClaimSync
611
+ function createHuskAuth: (deps: HuskAuthDeps) => HuskAuth
612
+ function createUnavailableClaimSync: (reason?: string) => ClaimSync
613
+ export interface EmulatorClaimSyncOptions { authHost: string projectId: string }
614
+ export interface HuskAuth { signIn(email: string, password: string): Promise<AuthUser> signUp(input: SignUpInput): Promise<AuthUser> signOut(): Promise<void> currentUser(): Promise<AuthUser | null> onChange( listener: (user: AuthUser | null) => void, onError?: (error: unknown) => void, ): () => void getUser(uid: string): Promise<StoredUser | null> updateProfile( uid: string, patch: Omit<UserPatch, "role" | "disabled">, ): Promise<StoredUser> setRole(uid: string, role: Role): Promise<StoredUser> setDisabled(uid: string, disabled: boolean): Promise<StoredUser> }
615
+ export interface HuskAuthDeps { auth: Auth db: Firestore claimSync: ClaimSync defaultRole?: Role }
616
+ export interface NewUser { uid: string email: string displayName?: string | null role: Role }
617
+ export type Role = (typeof ROLES)[number]
618
+ const ROLES: readonly ["admin", "editor", "author"]
619
+ export interface SignUpInput { email: string password: string displayName?: string }
620
+ export interface StoredUser { uid: string email: string displayName: string | null role: Role createdAt: string disabled: boolean }
621
+ function toAuthUser: (stored: StoredUser) => AuthUser
622
+ export interface UserPatch { displayName?: string | null role?: Role disabled?: boolean }
623
+ ```
624
+
625
+ ## `@nonext/husk/client`
626
+
627
+ 11 exports.
628
+
629
+ ```ts
630
+ export type ClientCmsConfig<TTypes extends ContentTypeMap = ContentTypeMap> = CmsReadConfig<TTypes>
631
+ export interface Cms<TTypes extends ContentTypeMap = ContentTypeMap> { collection<TSlug extends SlugOf<TTypes>>( slug: TSlug, ): CollectionReader<EntryOf<TTypes, TSlug>> single<TSlug extends SlugOf<TTypes>>(slug: TSlug): SingleReader<EntryOf<TTypes, TSlug>> media(): MediaReader settings(): SettingsReader readonly isPreview: boolean }
632
+ export type CmsCountQuery = Pick<CmsQuery, "filters" | "search">
633
+ export type CmsQuery = Omit<ListQuery, "status">
634
+ export interface CollectionReader<TEntry = Entry> { findMany(query?: CmsQuery): Promise<Page<TEntry>> findBySlug(value: string): Promise<TEntry | null> findById(entryId: string): Promise<TEntry | null> count(query?: CmsCountQuery): Promise<number> }
635
+ export type ContentTypeMap = { readonly [slug: string]: KindShape & AnyContentTypeShape }
636
+ function createClientCMS: <TTypes extends ContentTypeMap = ContentTypeMap>(config: ClientCmsConfig<TTypes>) => Cms<TTypes>
637
+ export type EntryOf< TTypes extends ContentTypeMap, TSlug extends string, > = TSlug extends keyof TTypes ? Entry<TTypes[TSlug]> : Entry
638
+ export interface MediaReader { getById(mediaId: string): Promise<MediaItem | null> getMany(mediaIds: readonly string[]): Promise<readonly MediaItem[]> }
639
+ export interface SettingsReader { get(key: string): Promise<Record<string, unknown> | null> }
640
+ export interface SingleReader<TEntry = Entry> { get(): Promise<TEntry | null> }
641
+ ```
642
+
643
+ ## `@nonext/husk/core`
644
+
645
+ 233 exports.
646
+
647
+ ```ts
648
+ function addField: (type: ContentTypeLike, key: string, field: FieldDefinition) => ContentTypeLike
649
+ const ALLOWED_MEDIA_TYPES: ReadonlyArray<MediaTypeRule>
650
+ function applyPlan: (plan: readonly EntryMigration[], values: Record<string, unknown>, context: Omit<MigrationContext, "fromVersion">) => Record<string, unknown>
651
+ function approximateBytes: (value: unknown) => number
652
+ function assertDeletable: (type: KindShape) => void
653
+ function assertListable: (type: KindShape) => void
654
+ function assertSchemaWritable: (registry: Registry, slug: string) => RegistryEntry
655
+ function assertSingletonCreatable: (type: KindShape, exists: boolean) => void
656
+ function assertSingletonEntryId: (type: KindShape, entryId: string) => void
657
+ function assertTypeSlugAvailable: (registry: Registry, slug: string) => void
658
+ function assertValidContentType: (definition: { slug: string; kind: unknown; fields: FieldMap; removedFields?: RemovedFieldMap | undefined; titleField?: string | undefined; defaultSort?: { field: string; direction: "asc" | "desc"; } | undefined; permissions?: unknown; }) => void
659
+ function buildCountQuery: (type: ContentTypeLike, state: ListViewState) => Pick<ListQuery, "filters" | "status" | "search">
660
+ function buildListQuery: (type: ContentTypeLike, state: ListViewState, { pageSize }?: BuildListQueryOptions) => ListQuery
661
+ export interface BuildListQueryOptions { pageSize?: number }
662
+ function buildRegistry: (code: readonly ContentTypeLike[], database: readonly ContentTypeLike[]) => Registry
663
+ function canSaveEntry: (user: AuthUser, type: ContentTypeLike, entry: StatusSubject | null) => boolean
664
+ function canSearch: (type: ContentTypeLike) => boolean
665
+ function checkFieldKey: (type: ContentTypeLike, key: string) => FieldKeyAvailability
666
+ function checkMediaFile: (file: MediaFileFacts) => MediaRejection | null
667
+ function checkNestedFieldKey: (fields: FieldMap, key: string) => FieldKeyAvailability
668
+ function checkTypeSlug: (registry: Registry, slug: string) => SlugAvailability
669
+ function checkWritePayload: (values: Record<string, unknown>, limits?: PayloadLimits) => WriteLimitRejection | null
670
+ export interface ClassifiedError { category: ErrorCategory treatment: ErrorTreatment title: string message: string fieldErrors: readonly FieldError[] retryable: boolean cause: unknown }
671
+ function classifyError: (thrown: unknown, context?: ErrorContext) => ClassifiedError
672
+ export type Clock = () => number
673
+ export interface Cms<TTypes extends ContentTypeMap = ContentTypeMap> { collection<TSlug extends SlugOf<TTypes>>( slug: TSlug, ): CollectionReader<EntryOf<TTypes, TSlug>> single<TSlug extends SlugOf<TTypes>>(slug: TSlug): SingleReader<EntryOf<TTypes, TSlug>> media(): MediaReader settings(): SettingsReader readonly isPreview: boolean }
674
+ export interface CmsAdapter { list(slug: string, query?: ListQuery): Promise<Page<Entry>> get(slug: string, entryId: string): Promise<Entry | null> getBySlug( slug: string, value: string, options?: Pick<ListQuery, "status">, ): Promise<Entry | null> getSingle(slug: string): Promise<Entry | null> create(slug: string, input: EntryInputData): Promise<Entry> update(slug: string, entryId: string, input: EntryInputData): Promise<Entry> delete(slug: string, entryId: string): Promise<void> count( slug: string, query?: Pick<ListQuery, "filters" | "status" | "search">, ): Promise<number> readonly media: MediaAdapter readonly schemas: SchemaAdapter readonly fieldData: FieldDataAdapter readonly migrations: MigrationAdapter readonly settings: SettingsAdapter }
675
+ export interface CmsConfig< TTypes extends ContentTypeMap = ContentTypeMap, > extends CmsReadConfig<TTypes> { preview?: boolean }
676
+ export type CmsCountQuery = Pick<CmsQuery, "filters" | "search">
677
+ export type CmsQuery = Omit<ListQuery, "status">
678
+ export interface CmsReadConfig<TTypes extends ContentTypeMap = ContentTypeMap> { adapter: CmsAdapter contentTypes?: TTypes }
679
+ export interface CollectionReader<TEntry = Entry> { findMany(query?: CmsQuery): Promise<Page<TEntry>> findBySlug(value: string): Promise<TEntry | null> findById(entryId: string): Promise<TEntry | null> count(query?: CmsCountQuery): Promise<number> }
680
+ function collectMediaIds: (data: Record<string, unknown>) => string[]
681
+ export type ColumnAlign = "text" | "numeric" | "date"
682
+ export type ColumnKind = "title" | "field" | "status" | "updated"
683
+ function confirmationMatches: (expected: string, typed: string) => boolean
684
+ export type ContentTypeMap = { readonly [slug: string]: KindShape & AnyContentTypeShape }
685
+ function contentTypeTag: (slug: string) => string
686
+ function createCMS: <TTypes extends ContentTypeMap = ContentTypeMap>(config: CmsConfig<TTypes>) => Cms<TTypes>
687
+ function createContentType: (registry: Registry, input: NewContentTypeInput) => ContentTypeLike
688
+ function createMigrationRegistry: (migrations: readonly EntryMigration[]) => MigrationRegistry
689
+ function createRegistryStore: (code: readonly ContentTypeLike[], initialDatabase?: readonly ContentTypeLike[]) => RegistryStore
690
+ function createWriteRateLimiter: (clock?: Clock) => WriteRateLimiter
691
+ const DEFAULT_PAGE_SIZE: 25
692
+ function defaultListSort: (type: ContentTypeLike) => SortSpec
693
+ function defineContentType: <const TFields extends FieldMap>(definition: ContentType<TFields>) => ContentType<TFields>
694
+ function deleteConfirmationName: (type: ContentTypeLike, entry: Entry) => string
695
+ function deleteConfirmationPhrase: (type: ContentTypeLike, count: number) => string
696
+ function deniedError: (what: string, role: Role | undefined) => ClassifiedError
697
+ function draftIssues: (registry: FieldTypeRegistry, rows: readonly FieldRow[]) => readonly (FieldOptionIssue & { key: string; })[]
698
+ const ENTRY_COLLECTION_GROUP: "entries"
699
+ function entryChangedSince: (baseline: Pick<EntryMeta, "updatedAt">, current: Pick<EntryMeta, "updatedAt">) => boolean
700
+ export interface EntryFilter { field: string op: FilterOperator value: unknown }
701
+ export interface EntryInputData { values: Record<string, unknown> status?: EntryStatus }
702
+ export interface EntryMigration { slug: string from: number to: number describe: string migrate( values: Record<string, unknown>, context: MigrationContext, ): Record<string, unknown> }
703
+ export type EntryOf< TTypes extends ContentTypeMap, TSlug extends string, > = TSlug extends keyof TTypes ? Entry<TTypes[TSlug]> : Entry
704
+ function entrySlugTag: (slug: string, value: string) => string
705
+ function entryTag: (slug: string, entryId: string) => string
706
+ function entryTags: (slug: string, entryId: string) => readonly string[]
707
+ function entryTitle: (type: ContentTypeLike, entry: Entry) => string
708
+ function entryUniqueCheck: (options: EntryUniqueOptions) => (path: string, value: string) => Promise<boolean>
709
+ export interface EntryUniqueOptions { adapter: CmsAdapter type: ContentTypeLike entryId: string | null }
710
+ const ERROR_CATEGORIES: readonly ["validation", "permission", "not-found", "conflict", "transport", "unknown"]
711
+ export type ErrorCategory = (typeof ERROR_CATEGORIES)[number]
712
+ export interface ErrorContext { role?: Role | undefined action?: string | undefined subject?: string | undefined }
713
+ export type ErrorOrigin = "client" | "server"
714
+ function errorsUnder: <TError extends FieldError>(errors: readonly TError[], path: string) => TError[]
715
+ export type ErrorTreatment = "field" | "banner" | "state" | "resolve" | "retry"
716
+ const FALLBACK_SORT: SortSpec
717
+ export interface FieldDataAdapter { count(slug: string, key: string): Promise<number> purge(slug: string, key: string, options?: FieldPurgeOptions): Promise<FieldPurgeResult> }
718
+ export interface FieldKeyAvailability { available: boolean reason?: FieldKeyRejection message?: string }
719
+ export type FieldKeyRejection = "invalid" | "reserved" | "live" | "removed"
720
+ function fieldKeyState: (type: ContentTypeLike, key: string) => FieldKeyState
721
+ export type FieldKeyState = "free" | "live" | "removed"
722
+ function fieldMapSignature: (fields: FieldMap) => string
723
+ export interface FieldOptionIssue { option: string message: string }
724
+ function fieldOptionIssues: (registry: FieldTypeRegistry, field: FieldDefinition) => readonly FieldOptionIssue[]
725
+ export interface FieldPurgeOptions { limit?: number }
726
+ export interface FieldPurgeResult { purged: number remaining: boolean }
727
+ export interface FieldRow { key: string field: FieldDefinition }
728
+ function fileExtension: (name: string) => string
729
+ export type FilterOperator = | "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "array-contains" | "array-contains-any"
730
+ const FIRESTORE_DOCUMENT_BYTES: 1048576
731
+ export interface FirestoreIndex { collectionGroup: string queryScope: "COLLECTION" fields: readonly FirestoreIndexField[] }
732
+ export interface FirestoreIndexField { fieldPath: string order: IndexOrder }
733
+ function forgetRemovedField: (type: ContentTypeLike, key: string) => ContentTypeLike
734
+ export interface FormField { key: string field: FieldDefinition }
735
+ function formFields: (type: ContentTypeLike) => readonly FormField[]
736
+ export interface FormIssue extends FieldError { origin: ErrorOrigin }
737
+ function formValuesFromEntry: (type: ContentTypeLike, entry: Entry, registry: FieldTypeRegistry) => Record<string, unknown>
738
+ const HUSK_TAG_PREFIX: "husk"
739
+ function indexCovered: (file: { indexes?: readonly FirestoreIndex[]; }, required: FirestoreIndex) => boolean
740
+ export type IndexOrder = "ASCENDING" | "DESCENDING"
741
+ const INITIAL_FIELD_KEY: "title"
742
+ const INITIAL_SCHEMA_VERSION: 1
743
+ function initialFormValues: (type: ContentTypeLike, registry: FieldTypeRegistry) => Record<string, unknown>
744
+ function initialListState: (type: ContentTypeLike) => ListViewState
745
+ export interface InvalidContentType { slug: string reason: string definition: unknown }
746
+ function isCollection: (type: KindShape) => boolean
747
+ function isFilterableField: (field: FieldDefinition) => boolean
748
+ function isHuskTag: (value: string) => boolean
749
+ function isNarrowed: (state: ListViewState) => boolean
750
+ function isSchemaReadOnly: (entry: RegistryEntry) => boolean
751
+ function isSearching: (state: ListViewState) => boolean
752
+ function isSingleton: (type: KindShape) => boolean
753
+ export type KnownSlug<TTypes extends ContentTypeMap> = keyof TTypes & string
754
+ export interface ListColumn { key: string label: string kind: ColumnKind align: ColumnAlign sortable: boolean }
755
+ export interface ListColumnOptions { maxFieldColumns?: number }
756
+ function listColumns: (type: ContentTypeLike, { maxFieldColumns }?: ListColumnOptions) => readonly ListColumn[]
757
+ export interface ListFilterDescriptor { key: string label: string options: readonly ListFilterOption[] op: EntryFilter["op"] valueType: "string" | "boolean" }
758
+ export interface ListFilterOption { value: string label: string }
759
+ function listFilterQuery: (type: ContentTypeLike, filters: Readonly<Record<string, string>>) => readonly EntryFilter[]
760
+ function listFilters: (type: ContentTypeLike) => readonly ListFilterDescriptor[]
761
+ export interface ListQuery { filters?: readonly EntryFilter[] sort?: SortSpec search?: string limit?: number cursor?: string | null status?: EntryStatus | "any" }
762
+ export interface ListViewState { search: string status: EntryStatus | "any" filters: Readonly<Record<string, string>> sort: SortSpec cursor: string | null }
763
+ export class LockedTypeError extends Error { override readonly name = "LockedTypeError" readonly slug: string constructor(slug: string, reason?: string) { super( reason ?? `Content type "${slug}" is locked. It is defined in code and cannot be changed through the Schema Builder or the adapter.`, ) this.slug = slug } }
764
+ const MAX_ARRAY_ITEMS: 1000
765
+ const MAX_CONTENT_TYPE_FIELDS: 100
766
+ const MAX_ENTRY_FIELDS: 200
767
+ const MAX_ENTRY_INDEXES: 200
768
+ const MAX_FIELD_COLUMNS: 2
769
+ const MAX_FIELD_PURGE: 250
770
+ const MAX_IMAGE_BYTES: number
771
+ const MAX_MEDIA_BYTES: number
772
+ const MAX_MEDIA_REFS: 100
773
+ const MAX_NESTING_DEPTH: 20
774
+ const MAX_PAGE_SIZE: 100
775
+ const MAX_SETTINGS_FIELDS: 100
776
+ const MAX_WRITE_BYTES: 900000
777
+ const MEDIA_ACCEPT_ATTRIBUTE: string
778
+ const MEDIA_REFS_KEY: "mediaRefs"
779
+ const MEDIA_USAGE_LIMIT: 25
780
+ export interface MediaAdapter { list(query?: MediaQuery): Promise<Page<MediaItem>> getById(mediaId: string): Promise<MediaItem | null> getMany(mediaIds: readonly string[]): Promise<readonly MediaItem[]> reserveId(): string create(input: MediaInput, mediaId?: string): Promise<MediaItem> update(mediaId: string, patch: Partial<MediaInput>): Promise<MediaItem> delete(mediaId: string): Promise<void> usages( mediaId: string, query?: MediaUsageQuery, ): Promise<readonly MediaUsageReference[]> refreshReferences(mediaId: string, url: string): Promise<MediaRefreshResult> usage(): Promise<MediaUsage> }
781
+ export type MediaCategory = "image" | "video" | "audio" | "document"
782
+ export interface MediaFileFacts { name: string type: string size: number }
783
+ export interface MediaInput { name: string type: string size: number url: string path: string alt?: string | null width?: number | null height?: number | null }
784
+ export interface MediaQuery { limit?: number cursor?: string | null type?: string }
785
+ export interface MediaReader { getById(mediaId: string): Promise<MediaItem | null> getMany(mediaIds: readonly string[]): Promise<readonly MediaItem[]> }
786
+ export interface MediaRefreshResult { updated: number failed: readonly MediaUsageReference[] }
787
+ export interface MediaRejection { code: MediaRejectionCode message: string }
788
+ export type MediaRejectionCode = "type-not-allowed" | "too-large" | "empty" | "extension-mismatch"
789
+ function mediaTag: (mediaId: string) => string
790
+ function mediaTypeRule: (type: string) => MediaTypeRule | null
791
+ export interface MediaTypeRule { readonly type: string readonly category: MediaCategory readonly maxBytes: number readonly extensions: readonly string[] }
792
+ export interface MediaUsage { count: number bytes: number }
793
+ export interface MediaUsageQuery { limit?: number }
794
+ export interface MediaUsageReference { contentType: string entryId: string title: string | null }
795
+ function mergeIssues: (client: readonly FieldError[], server: readonly FieldError[]) => readonly FormIssue[]
796
+ const MIGRATION_BATCH_SIZE: 50
797
+ const MIGRATION_MAX_BATCHES: 20
798
+ export interface MigrationAdapter { countBehind(slug: string, targetVersion: number): Promise<number> listBehind( slug: string, targetVersion: number, query?: { limit?: number }, ): Promise<Page<StoredEntryRecord>> applyMigration( slug: string, entryId: string, values: Record<string, unknown>, write: MigrationWrite, ): Promise<boolean> }
799
+ export interface MigrationContext { entryId: string slug: string fromVersion: number }
800
+ export interface MigrationFailure { entryId: string fromVersion: number reason: string }
801
+ export interface MigrationRegistry { forType(slug: string): readonly EntryMigration[] plan(type: ContentTypeLike, fromVersion: number): readonly EntryMigration[] }
802
+ export interface MigrationRunDeps { migrations: MigrationAdapter registry: MigrationRegistry type: ContentTypeLike }
803
+ export interface MigrationRunOptions { batchSize?: number maxBatches?: number onProgress?: (progress: { scanned: number; migrated: number }) => void }
804
+ export interface MigrationRunResult { slug: string targetVersion: number scanned: number migrated: number skipped: number failed: readonly MigrationFailure[] remaining: boolean }
805
+ export interface MigrationWrite { expectedVersion: number nextVersion: number }
806
+ const MIN_WRITE_INTERVAL_MS: 200
807
+ function missingEntryIndexes: (file: { indexes?: readonly FirestoreIndex[]; }, types: readonly ContentTypeLike[]) => readonly FirestoreIndex[]
808
+ function missingError: (subject: string) => ClassifiedError
809
+ function moveFieldRow: (rows: readonly FieldRow[], from: number, to: number) => FieldRow[]
810
+ function moveItem: <TItem>(items: readonly TItem[], from: number, to: number) => TItem[]
811
+ function newContentType: (input: NewContentTypeInput) => ContentTypeLike
812
+ export interface NewContentTypeInput { name: string slug: string kind: ContentTypeKind icon?: string namePlural?: string description?: string }
813
+ function newFieldDefinition: (registry: FieldTypeRegistry, type: FieldType, label: string) => FieldDefinition
814
+ function nextSchemaVersion: (previous: ContentTypeLike | null, next: ContentTypeLike) => number
815
+ function nextSort: (current: SortSpec, column: ListColumn, key?: string) => SortSpec
816
+ function normalizeConfirmation: (value: string) => string
817
+ function normalizeLimit: (limit: number | undefined) => number
818
+ function normalizeMediaType: (type: string) => string
819
+ function orderedFieldKeys: (type: Pick<ContentTypeLike, "fields" | "fieldOrder">) => readonly string[]
820
+ function orderedFields: (type: Pick<ContentTypeLike, "fields" | "fieldOrder">) => readonly FieldRow[]
821
+ export interface Page<TItem> { items: readonly TItem[] cursor: string | null hasMore: boolean }
822
+ export interface PayloadLimits { maxFields?: number maxBytes?: number }
823
+ export class PermissionError extends Error { override readonly name = "PermissionError" readonly action: string readonly contentType: string | undefined constructor(action: string, contentType?: string, message?: string) { super( message ?? (contentType === undefined ? `This account may not ${action}.` : `This account may not ${action} entries of "${contentType}".`), ) this.action = action this.contentType = contentType } }
824
+ export interface Registry { readonly types: readonly RegistryEntry[] readonly conflicts: readonly RegistryEntry[] readonly invalid: readonly InvalidContentType[] get(slug: string): RegistryEntry | undefined require(slug: string): RegistryEntry has(slug: string): boolean bySource(source: ContentTypeSource): readonly RegistryEntry[] }
825
+ export interface RegistryEntry { type: ContentTypeLike source: ContentTypeSource locked: boolean conflict?: { source: "database"; type: ContentTypeLike } }
826
+ export interface RegistryStore { readonly registry: Registry setDatabaseTypes(types: readonly ContentTypeLike[]): boolean subscribe(listener: (registry: Registry) => void): () => void readonly revision: number }
827
+ export interface RemovalContext { removedBy: string removedAt?: string }
828
+ function removedFieldEntries: (type: ContentTypeLike) => readonly (RemovedField & { key: string; })[]
829
+ function removeField: (type: ContentTypeLike, key: string, context: RemovalContext) => ContentTypeLike
830
+ function requiredEntryIndexes: (types: readonly ContentTypeLike[]) => readonly FirestoreIndex[]
831
+ const RESERVED_TYPE_SLUGS: ReadonlyArray<string>
832
+ function restoreField: (type: ContentTypeLike, key: string, options?: RestoreOptions) => ContentTypeLike
833
+ export interface RestoreOptions { field?: FieldDefinition }
834
+ function retargetMediaUrls: (data: Record<string, unknown>, mediaId: string, url: string) => Record<string, unknown> | null
835
+ function runMigration: (deps: MigrationRunDeps, options?: MigrationRunOptions) => Promise<MigrationRunResult>
836
+ function safeFileName: (name: string, type: string) => string
837
+ function sameDefinition: (left: ContentTypeLike, right: ContentTypeLike) => boolean
838
+ export interface SchemaAdapter { list(): Promise<readonly ContentTypeLike[]> get(slug: string): Promise<ContentTypeLike | null> create(type: ContentTypeLike): Promise<ContentTypeLike> update(slug: string, type: ContentTypeLike): Promise<ContentTypeLike> delete(slug: string): Promise<void> watch( listener: (types: readonly ContentTypeLike[]) => void, onError?: (error: Error) => void, ): () => void }
839
+ export class SchemaError extends Error { override readonly name = "SchemaError" constructor(message: string) { super(message) } }
840
+ function schemaVersionOf: (type: Pick<ContentTypeLike, "schemaVersion">) => number
841
+ export interface SettingsAdapter { get(key: string): Promise<Record<string, unknown> | null> set(key: string, value: Record<string, unknown>): Promise<void> }
842
+ export interface SettingsReader { get(key: string): Promise<Record<string, unknown> | null> }
843
+ function settingsTag: (key: string) => string
844
+ export interface SingleReader<TEntry = Entry> { get(): Promise<TEntry | null> }
845
+ function singletonEntryId: (slug: string) => string
846
+ export class SingletonError extends Error { override readonly name = "SingletonError" readonly slug: string constructor(slug: string, message: string) { super(message) this.slug = slug } }
847
+ export interface SlugAvailability { available: boolean reason?: SlugRejection message?: string source?: ContentTypeSource }
848
+ function slugFieldKey: (type: ContentTypeLike) => string | null
849
+ export type SlugOf<TTypes extends ContentTypeMap> = KnownSlug<TTypes> | (string & Record<never, never>)
850
+ export type SlugRejection = "invalid" | "reserved" | "taken-by-code" | "taken-by-database"
851
+ function sortableFieldKeys: (type: ContentTypeLike) => readonly string[]
852
+ export interface SortSpec { field: string direction: "asc" | "desc" }
853
+ function staleWriteError: (subject: string) => ClassifiedError
854
+ const STATUS_EFFECT: Readonly<Record<"draft" | "published" | "archived", string>>
855
+ export type StatusSubject = Pick<EntryMeta, "status" | "createdBy">
856
+ export interface StatusTransition { to: EntryStatus label: string effect: string allowed: boolean reason: string | null }
857
+ function statusTransitions: (user: AuthUser, type: ContentTypeLike, entry: StatusSubject | null) => readonly StatusTransition[]
858
+ function storageShapeChanged: (previous: ContentTypeLike, next: ContentTypeLike) => boolean
859
+ function storageSignature: (type: Pick<ContentTypeLike, "kind" | "fields">) => string
860
+ function storageSignatureOf: (field: FieldDefinition) => string
861
+ export interface StoredEntryRecord { id: string schemaVersion: number values: Record<string, unknown> }
862
+ function storesTheSameWay: (left: FieldDefinition, right: FieldDefinition) => boolean
863
+ function suggestFieldKey: (label: string) => string
864
+ function treatmentFor: (category: ErrorCategory) => ErrorTreatment
865
+ function unroutableIssues: <TError extends FieldError>(issues: readonly TError[], type: ContentTypeLike) => readonly TError[]
866
+ const UNTITLED_ENTRY: "Untitled"
867
+ const VALIDATION_RULE_CONTROLS: { readonly required: ValidationRuleControl; readonly minLength: ValidationRuleControl; readonly maxLength: ValidationRuleControl; readonly min: ValidationRuleControl; readonly max: ValidationRuleControl; readonly pattern: ValidationRuleControl; readonly unique: ValidationRuleControl; }
868
+ export class ValidationError extends Error { override readonly name = "ValidationError" readonly errors: readonly FieldError[] constructor(errors: readonly FieldError[]) { super( errors.length === 1 && errors[0] ? `${errors[0].path}: ${errors[0].message}` : `${errors.length} fields failed validation`, ) this.errors = errors } }
869
+ export interface ValidationRuleControl { label: string control: "boolean" | "number" | "text" help: string }
870
+ export type ValidationRuleName = keyof ValidationRules
871
+ function valuesChanged: (baseline: Record<string, unknown>, current: Record<string, unknown>) => boolean
872
+ function withFieldOption: (field: FieldDefinition, option: string, value: unknown) => FieldDefinition
873
+ function withFieldRows: (type: ContentTypeLike, rows: readonly FieldRow[]) => ContentTypeLike
874
+ function withValidationRule: (field: FieldDefinition, rule: ValidationRuleName, value: string | number | boolean | undefined) => FieldDefinition
875
+ const WRITE_BURST_LIMIT: 200
876
+ const WRITE_BURST_WINDOW_MS: 10000
877
+ export type WriteLimitCode = "too-many-fields" | "too-large" | "too-deep" | "too-many-items" | "too-fast"
878
+ export class WriteLimitError extends Error { override readonly name = "WriteLimitError" readonly code: string readonly path: string constructor(rejection: { code: string; path: string; message: string }) { super(rejection.message) this.code = rejection.code this.path = rejection.path } }
879
+ export interface WriteLimitRejection { code: WriteLimitCode path: string message: string }
880
+ export interface WriteRateLimiter { check(): WriteLimitRejection | null }
881
+ ```
882
+
883
+ ## `@nonext/husk/firebase`
884
+
885
+ 36 exports.
886
+
887
+ ```ts
888
+ export class AuthError extends Error { override readonly name = "AuthError" readonly code: AuthErrorCode constructor(code: AuthErrorCode, message: string) { super(message) this.code = code } }
889
+ export type AuthErrorCode = "disabled" | "invalid-credentials"
890
+ const browserImageSize: ImageSizeReader
891
+ export interface ClaimSet { role: Role disabled: boolean }
892
+ export interface ClaimSync { sync(uid: string, claims: ClaimSet): Promise<void> }
893
+ export class ClaimSyncError extends Error { override readonly name = "ClaimSyncError" readonly uid: string constructor(uid: string, message: string) { super(message) this.uid = uid } }
894
+ function createEmulatorClaimSync: (options: EmulatorClaimSyncOptions) => ClaimSync
895
+ function createFirestoreAdapter: (deps: FirestoreAdapterDeps) => CmsAdapter
896
+ function createHuskAuth: (deps: HuskAuthDeps) => HuskAuth
897
+ function createMediaReplace: (deps: MediaUploadDeps, existing: MediaItem, file: File, options?: MediaUploadOptions) => MediaUpload
898
+ function createMediaUpload: (deps: MediaUploadDeps, file: File, options?: MediaUploadOptions) => MediaUpload
899
+ function createMediaUploader: (deps: MediaUploadDeps) => MediaUploader
900
+ function createUnavailableClaimSync: (reason?: string) => ClaimSync
901
+ export interface EmulatorClaimSyncOptions { authHost: string projectId: string }
902
+ export interface FirestoreAdapterDeps { db: Firestore registry: () => Registry currentUser: () => AuthUser session?: () => AuthUser | null fieldTypes?: FieldTypeRegistry rateLimiter?: WriteRateLimiter | null }
903
+ export interface HuskAuth { signIn(email: string, password: string): Promise<AuthUser> signUp(input: SignUpInput): Promise<AuthUser> signOut(): Promise<void> currentUser(): Promise<AuthUser | null> onChange( listener: (user: AuthUser | null) => void, onError?: (error: unknown) => void, ): () => void getUser(uid: string): Promise<StoredUser | null> updateProfile( uid: string, patch: Omit<UserPatch, "role" | "disabled">, ): Promise<StoredUser> setRole(uid: string, role: Role): Promise<StoredUser> setDisabled(uid: string, disabled: boolean): Promise<StoredUser> }
904
+ export interface HuskAuthDeps { auth: Auth db: Firestore claimSync: ClaimSync defaultRole?: Role }
905
+ export interface ImageSize { width: number height: number }
906
+ export type ImageSizeReader = (file: Blob, type: string) => Promise<ImageSize | null>
907
+ export interface MediaUpload { readonly mediaId: string readonly path: string readonly fileName: string progress(): UploadProgress start(): Promise<MediaItem> retry(): Promise<MediaItem> cancel(): void }
908
+ export interface MediaUploadDeps { storage: FirebaseStorage media: MediaAdapter currentUser: () => AuthUser readImageSize?: ImageSizeReader }
909
+ export class MediaUploadError extends Error { override readonly name = "MediaUploadError" readonly code: MediaUploadErrorCode readonly rejection: MediaRejection | null readonly orphanedPath: string | null constructor( code: MediaUploadErrorCode, message: string, detail: { rejection?: MediaRejection; orphanedPath?: string; cause?: unknown } = {}, ) { super(message, detail.cause === undefined ? undefined : { cause: detail.cause }) this.code = code this.rejection = detail.rejection ?? null this.orphanedPath = detail.orphanedPath ?? null } }
910
+ export type MediaUploadErrorCode = "rejected" | "cancelled" | "storage-failed" | "metadata-failed"
911
+ export interface MediaUploadOptions { onProgress?: (progress: UploadProgress) => void alt?: string | null }
912
+ export interface NewUser { uid: string email: string displayName?: string | null role: Role }
913
+ function patchUser: (db: Firestore, uid: string, patch: UserPatch) => Promise<StoredUser>
914
+ function readUser: (db: Firestore, uid: string) => Promise<StoredUser | null>
915
+ function removeOrphan: (storage: FirebaseStorage, path: string) => Promise<boolean>
916
+ export interface SignUpInput { email: string password: string displayName?: string }
917
+ export interface StoredUser { uid: string email: string displayName: string | null role: Role createdAt: string disabled: boolean }
918
+ function toAuthUser: (stored: StoredUser) => AuthUser
919
+ function uploadMedia: (deps: MediaUploadDeps, file: File, options?: MediaUploadOptions) => Promise<MediaItem>
920
+ export type UploadPhase = "idle" | "uploading" | "storing" | "done" | "cancelled" | "failed"
921
+ export interface UploadProgress { phase: UploadPhase bytesTransferred: number totalBytes: number fraction: number attempt: number }
922
+ export interface UserPatch { displayName?: string | null role?: Role disabled?: boolean }
923
+ function writeUser: (db: Firestore, user: NewUser) => Promise<StoredUser>
924
+ ```
925
+
926
+ ## `@nonext/husk/permissions`
927
+
928
+ 20 exports.
929
+
930
+ ```ts
931
+ const ACTIONS: ReadonlyArray<Action>
932
+ const ADMIN_ACTIONS: readonly ["manage-schema", "manage-users", "manage-settings", "manage-media"]
933
+ function assertValidPermissionMatrix: (slug: string, matrix: unknown) => void
934
+ function can: (user: AuthUser, action: Action, context?: PermissionContext) => boolean
935
+ export type Decision = "allow" | "deny" | "own-entry-only"
936
+ function defaultRoles: (action: EntryAction) => readonly Role[]
937
+ export type EntryOwnership = Pick<EntryMeta, "createdBy">
938
+ function isEntryAction: (action: Action) => action is EntryAction
939
+ const MATRIX_ACTIONS: ReadonlyArray<EntryAction>
940
+ const MATRIX_LIMITS: { readonly admin: "An admin is never restricted by a matrix. Removing every other role from an action leaves it admin-only; there is no way to lock an admin out of their own project."; readonly ownership: "An author may only ever update or delete entries they created. Ticking an author here grants the action on their own entries, never on somebody else's."; readonly narrowing: "A matrix can only take permissions away. Ticking a role that its own role does not grant changes nothing, and the row says so."; readonly publicRead: "Denying read hides entries from the admin, not from the public website. A published entry is readable by anyone with its address by design; unpublish it to take it down."; }
941
+ export interface MatrixCell { role: Role listed: boolean decision: MatrixDecision fixed: boolean }
942
+ function matrixDecision: (type: Pick<ContentTypeLike, "permissions"> | undefined, role: Role, action: EntryAction) => MatrixDecision
943
+ export type MatrixDecision = Decision
944
+ export interface MatrixRow { action: EntryAction cells: readonly MatrixCell[] }
945
+ function normalizePermissionMatrix: (matrix: PermissionMatrix | undefined) => PermissionMatrix | undefined
946
+ const PERMISSION_DEFAULTS: Readonly<Record<"admin" | "editor" | "author", Readonly<Record<Action, Decision>>>>
947
+ export interface PermissionContext { type?: ContentTypeLike entry?: EntryOwnership }
948
+ function permissionMatrixRows: (type: Pick<ContentTypeLike, "permissions"> | undefined) => readonly MatrixRow[]
949
+ function resolveDecision: (role: Role, action: Action, type: Pick<ContentTypeLike, "permissions"> | undefined) => Decision
950
+ function withMatrixRole: (matrix: PermissionMatrix | undefined, action: EntryAction, role: Role, listed: boolean) => PermissionMatrix | undefined
951
+ ```
952
+
953
+ ## `@nonext/husk/richtext`
954
+
955
+ 9 exports.
956
+
957
+ ```ts
958
+ function isEmptyRichTextContent: (content: unknown) => boolean
959
+ function isRichTextValue: (raw: unknown) => raw is RichTextValue
960
+ function RichText: ({ value }: RichTextProps) => ReactNode
961
+ const RICHTEXT_FORMAT: "husk-richtext-v1"
962
+ export interface RichTextNodeJSON { type?: string attrs?: Record<string, unknown> content?: RichTextNodeJSON[] marks?: { type: string; attrs?: Record<string, unknown> }[] text?: string }
963
+ export interface RichTextProps { value: RichTextValue | null }
964
+ function richTextToPlainText: (content: unknown) => string
965
+ export interface RichTextValue { format: "husk-richtext-v1" content: unknown }
966
+ function safeHref: (value: unknown) => string | null
967
+ ```
968
+
969
+ ## `@nonext/husk/schema`
970
+
971
+ 19 exports.
972
+
973
+ ```ts
974
+ export type AnyFieldTypeSpec = FieldTypeSpec<any, any>
975
+ export interface FieldEditorSpec { label: string description: string rules: readonly (keyof ValidationRules)[] options: readonly FieldOptionDescriptor[] defaults: Record<string, unknown> }
976
+ export type FieldInputComponent< TValue, TField extends FieldDefinition = FieldDefinition, > = ComponentType<FieldInputProps<TValue, TField>>
977
+ export interface FieldInputProps< TValue, TField extends FieldDefinition = FieldDefinition, > { path: string field: TField value: TValue | null onChange: (value: TValue | null) => void onBlur?: (() => void) | undefined errors: readonly FieldError[] disabled?: boolean | undefined readonly?: boolean | undefined }
978
+ export type FieldOptionControl = | "text" | "number" | "boolean" | "choice" | "select-options" | "content-type" | "field-key" | "string-list" | "fields"
979
+ export interface FieldOptionDescriptor { key: string label: string control: FieldOptionControl help?: string required?: boolean choices?: readonly { value: string; label: string }[] }
980
+ export type FieldTsType<TField extends FieldDefinition = FieldDefinition> = string | ((field: TField, resolve: TsTypeResolver) => string)
981
+ export interface FieldTypeRegistry { get(name: string): AnyFieldTypeSpec | undefined require(name: string): AnyFieldTypeSpec has(name: string): boolean readonly names: readonly FieldType[] }
982
+ export interface FieldTypeSpec<TValue, TField extends FieldDefinition = FieldDefinition> { name: FieldType toStored(value: TValue | null, field: TField, nested?: NestedConverter): unknown fromStored(raw: unknown, field: TField, nested?: NestedConverter): TValue | null Input: FieldInputComponent<TValue, TField> validate( value: TValue | null, rules: ValidationRules, ctx: ValidateCtx, field: TField, ): FieldError[] tsType: FieldTsType<TField> defaultValue(field: TField): TValue | null editor: FieldEditorSpec }
983
+ export interface NestedConverter { toStored(fields: FieldMap, values: Record<string, unknown>): Record<string, unknown> fromStored(fields: FieldMap, data: Record<string, unknown>): Record<string, unknown> }
984
+ const PATTERN_PRESETS: { readonly email: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"; readonly slug: "^[a-z0-9]+(?:-[a-z0-9]+)*$"; readonly url: "^https?://[^\\s]+$"; }
985
+ function registerFieldTypes: (specs: readonly AnyFieldTypeSpec[]) => FieldTypeRegistry
986
+ function resolveTsType: (registry: FieldTypeRegistry, field: FieldDefinition) => string
987
+ export type TsTypeResolver = (field: FieldDefinition) => string
988
+ function uniqueFieldPaths: (type: AnyContentTypeShape) => string[]
989
+ const UNKNOWN_TS_TYPE: "unknown"
990
+ export interface ValidateCtx { path: string contentType: AnyContentTypeShape entry: Record<string, unknown> }
991
+ function validateEntry: (type: AnyContentTypeShape, values: Record<string, unknown>, options?: ValidateOptions) => FieldError[]
992
+ export interface ValidateOptions { skipRequired?: boolean fieldTypes?: FieldTypeRegistry }
993
+ ```
994
+
995
+ ## `@nonext/husk/server`
996
+
997
+ 12 exports.
998
+
999
+ ```ts
1000
+ export interface Cms<TTypes extends ContentTypeMap = ContentTypeMap> { collection<TSlug extends SlugOf<TTypes>>( slug: TSlug, ): CollectionReader<EntryOf<TTypes, TSlug>> single<TSlug extends SlugOf<TTypes>>(slug: TSlug): SingleReader<EntryOf<TTypes, TSlug>> media(): MediaReader settings(): SettingsReader readonly isPreview: boolean }
1001
+ export type CmsCountQuery = Pick<CmsQuery, "filters" | "search">
1002
+ export type CmsQuery = Omit<ListQuery, "status">
1003
+ export interface CollectionReader<TEntry = Entry> { findMany(query?: CmsQuery): Promise<Page<TEntry>> findBySlug(value: string): Promise<TEntry | null> findById(entryId: string): Promise<TEntry | null> count(query?: CmsCountQuery): Promise<number> }
1004
+ export type ContentTypeMap = { readonly [slug: string]: KindShape & AnyContentTypeShape }
1005
+ function createServerCMS: <TTypes extends ContentTypeMap = ContentTypeMap>(config: ServerCmsConfig<TTypes>) => ServerCms<TTypes>
1006
+ export type EntryOf< TTypes extends ContentTypeMap, TSlug extends string, > = TSlug extends keyof TTypes ? Entry<TTypes[TSlug]> : Entry
1007
+ export interface MediaReader { getById(mediaId: string): Promise<MediaItem | null> getMany(mediaIds: readonly string[]): Promise<readonly MediaItem[]> }
1008
+ export interface ServerCms< TTypes extends ContentTypeMap = ContentTypeMap, > extends Cms<TTypes> { preview(): ServerCms<TTypes> }
1009
+ export type ServerCmsConfig<TTypes extends ContentTypeMap = ContentTypeMap> = CmsConfig<TTypes>
1010
+ export interface SettingsReader { get(key: string): Promise<Record<string, unknown> | null> }
1011
+ export interface SingleReader<TEntry = Entry> { get(): Promise<TEntry | null> }
1012
+ ```
1013
+
1014
+ ## `@nonext/husk/types`
1015
+
1016
+ 51 exports.
1017
+
1018
+ ```ts
1019
+ export type Action = EntryAction | AdminAction
1020
+ export type AdminAction = "manage-schema" | "manage-users" | "manage-settings" | "manage-media"
1021
+ export type AnyContentTypeShape = { fields: FieldMap }
1022
+ export interface AuthUser { uid: string email: string displayName: string | null role: Role disabled: boolean }
1023
+ export interface BooleanField extends FieldCommon { type: "boolean" defaultValue?: boolean }
1024
+ export interface ContentType<TFields extends FieldMap = FieldMap> { slug: string kind: ContentTypeKind label: string labelPlural?: string description?: string icon?: string fields: TFields fieldOrder?: readonly string[] removedFields?: RemovedFieldMap locked?: boolean titleField?: keyof TFields & string defaultSort?: { field: keyof TFields & string; direction: "asc" | "desc" } permissions?: PermissionMatrix schemaVersion?: number }
1025
+ export type ContentTypeInputValues<TType extends { fields: FieldMap }> = FieldInputValues< TType["fields"] >
1026
+ export type ContentTypeKind = "collection" | "singleton"
1027
+ export interface ContentTypeLike { slug: string kind: ContentTypeKind label: string labelPlural?: string description?: string icon?: string fields: FieldMap fieldOrder?: readonly string[] removedFields?: RemovedFieldMap locked?: boolean titleField?: string defaultSort?: { field: string; direction: "asc" | "desc" } permissions?: PermissionMatrix schemaVersion?: number }
1028
+ export type ContentTypeSource = "code" | "database"
1029
+ export type ContentTypeValues<TType extends { fields: FieldMap }> = FieldValues< TType["fields"] >
1030
+ export interface DateField extends FieldCommon { type: "date" precision?: "date" | "datetime" }
1031
+ export type Entry<TType extends AnyContentTypeShape = AnyContentTypeShape> = EntryMeta & ContentTypeValues<TType>
1032
+ const ENTRY_ACTIONS: readonly ["read", "create", "update", "delete"]
1033
+ const ENTRY_STATUSES: readonly ["draft", "published", "archived"]
1034
+ export type EntryAction = "read" | "create" | "update" | "delete"
1035
+ export type EntryInput<TType extends AnyContentTypeShape = AnyContentTypeShape> = ContentTypeInputValues<TType> & { status?: EntryStatus }
1036
+ export interface EntryMeta { id: string status: EntryStatus createdAt: string createdBy: string updatedAt: string updatedBy: string publishedAt: string | null schemaVersion: number }
1037
+ export type EntryStatus = (typeof ENTRY_STATUSES)[number]
1038
+ const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "select", "date", "slug", "richtext", "image", "file", "relation", "repeater"]
1039
+ export type FieldDefinition = | TextField | TextareaField | NumberField | BooleanField | SelectField | DateField | SlugField | RichtextField | ImageField | FileField | RelationField | RepeaterField
1040
+ export interface FieldError { path: string rule: string message: string }
1041
+ export type FieldInputValues<TFields extends FieldMap> = { [K in keyof TFields as IsRequired<TFields[K]> extends true ? K : never]: FieldValue< TFields[K] > } & { [K in keyof TFields as IsRequired<TFields[K]> extends true ? never : K]?: FieldValue< TFields[K] > | null }
1042
+ export type FieldMap = { readonly [key: string]: FieldDefinition }
1043
+ export type FieldType = (typeof FIELD_TYPES)[number]
1044
+ export 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 { multiple: true } ? TOption[] : TOption : TField extends RichtextField ? RichTextValue : TField extends ImageField | FileField ? MediaReference : TField extends RelationField ? TField extends { many: true } ? string[] : string : TField extends RepeaterField<infer TFields> ? FieldValues<TFields>[] : never
1045
+ export type FieldValues<TFields extends FieldMap> = { [K in keyof TFields]: IsRequired<TFields[K]> extends true ? FieldValue<TFields[K]> : FieldValue<TFields[K]> | null }
1046
+ export interface FileField extends FieldCommon { type: "file" accept?: readonly string[] }
1047
+ export interface HuskConfig { generatedWith: string adminRoute: string projectId: string storageBucket: string media: boolean auth: boolean emulators: boolean }
1048
+ export interface ImageField extends FieldCommon { type: "image" accept?: readonly string[] }
1049
+ export interface MediaItem { id: string name: string type: string size: number url: string path: string alt: string | null width: number | null height: number | null createdAt: string createdBy: string }
1050
+ export interface MediaReference { mediaId: string url: string }
1051
+ export interface NumberField extends FieldCommon { type: "number" step?: number defaultValue?: number }
1052
+ export interface Permission { action: Action contentType?: string }
1053
+ export type PermissionMatrix = Partial<Record<EntryAction, readonly Role[]>>
1054
+ export interface RelationField<TTo extends string = string> extends FieldCommon { type: "relation" to: TTo many?: boolean }
1055
+ export interface RemovedField { field: FieldDefinition removedAt: string removedBy: string }
1056
+ export type RemovedFieldMap = { readonly [key: string]: RemovedField }
1057
+ export interface RepeaterField<TFields extends FieldMap = FieldMap> extends FieldCommon { type: "repeater" fields: TFields min?: number max?: number }
1058
+ const RESERVED_FIELD_KEYS: readonly ["id", "status", "createdAt", "createdBy", "updatedAt", "updatedBy", "publishedAt", "schemaVersion"]
1059
+ export type ReservedFieldKey = (typeof RESERVED_FIELD_KEYS)[number]
1060
+ export interface RichtextField extends FieldCommon { type: "richtext" features?: readonly string[] }
1061
+ export interface RichTextValue { format: "husk-richtext-v1" content: unknown }
1062
+ export type Role = (typeof ROLES)[number]
1063
+ const ROLES: readonly ["admin", "editor", "author"]
1064
+ export interface SelectField<TValue extends string = string> extends FieldCommon { type: "select" options: readonly SelectOption<TValue>[] multiple?: boolean defaultValue?: TValue }
1065
+ export interface SelectOption<TValue extends string = string> { value: TValue label: string }
1066
+ export interface SlugField extends FieldCommon { type: "slug" from?: string }
1067
+ export interface TextareaField extends FieldCommon { type: "textarea" rows?: number defaultValue?: string }
1068
+ export interface TextField extends FieldCommon { type: "text" defaultValue?: string }
1069
+ export interface ValidationRules { required?: boolean minLength?: number maxLength?: number min?: number max?: number pattern?: string unique?: boolean }
1070
+ ```