@protobase/schema 0.21.7 → 0.21.9

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.
@@ -1,7 +1,7 @@
1
+ import { addressPlaceholders } from "./address-template.js";
1
2
  import { parseFilter } from "./filter/parse.js";
2
3
  import { checkFilter } from "./filter/check.js";
3
4
  import { checkOrderBy, parseOrderBy } from "./filter/order-by.js";
4
- import { addressPlaceholders } from "./address-template.js";
5
5
  //#region src/address-errors.ts
6
6
  const nameErrors = [
7
7
  "unknown-field",
@@ -0,0 +1,8 @@
1
+ //#region src/ai-prompt.d.ts
2
+ /** The fields an AI prompt reads from the record, from its `{field}` placeholders. */
3
+ export declare const promptFields: (prompt: string) => string[];
4
+ /** `prompt` with every `{field}` replaced by the record's value as text; a missing value becomes empty, json as JSON. */
5
+ export declare const renderPrompt: (prompt: string, record: Record<string, unknown>) => string;
6
+ /** Why a prompt cannot be an AI field's or action's: it reads no field of the record. */
7
+ export declare const promptError: (prompt: string) => "its prompt must name at least one {field} of the record" | undefined;
8
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { hrefPlaceholders } from "./address-template.js";
2
+ //#region src/ai-prompt.ts
3
+ const placeholder = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
4
+ /** The fields an AI prompt reads from the record, from its `{field}` placeholders. */
5
+ const promptFields = (prompt) => hrefPlaceholders(prompt);
6
+ const asText = (value) => {
7
+ if (value === null || value === void 0) return "";
8
+ if (typeof value === "string") return value;
9
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
10
+ return JSON.stringify(value);
11
+ };
12
+ /** `prompt` with every `{field}` replaced by the record's value as text; a missing value becomes empty, json as JSON. */
13
+ const renderPrompt = (prompt, record) => prompt.replace(placeholder, (_, name) => asText(record[name]));
14
+ /** Why a prompt cannot be an AI field's or action's: it reads no field of the record. */
15
+ const promptError = (prompt) => promptFields(prompt).length === 0 ? "its prompt must name at least one {field} of the record" : void 0;
16
+ //#endregion
17
+ export { promptError, promptFields, renderPrompt };
@@ -15,6 +15,8 @@ export declare const addressWords: {
15
15
  readonly filterLayout: "layout";
16
16
  /** Where a list's map display looks: `latitude,longitude,zoom`. */
17
17
  readonly map: "map";
18
+ /** The month or week a list's calendar display shows: `month,YYYY-MM-DD` or `week,YYYY-MM-DD`. */
19
+ readonly calendar: "calendar";
18
20
  /** The saved search a list was opened from, by its key. */
19
21
  readonly savedSearch: "saved_search";
20
22
  /** The second path segment that opens a resource's create form instead of a record. */
@@ -15,6 +15,8 @@ const addressWords = {
15
15
  filterLayout: "layout",
16
16
  /** Where a list's map display looks: `latitude,longitude,zoom`. */
17
17
  map: "map",
18
+ /** The month or week a list's calendar display shows: `month,YYYY-MM-DD` or `week,YYYY-MM-DD`. */
19
+ calendar: "calendar",
18
20
  /** The saved search a list was opened from, by its key. */
19
21
  savedSearch: "saved_search",
20
22
  /** The second path segment that opens a resource's create form instead of a record. */
@@ -41,6 +43,23 @@ const readMap = (text) => {
41
43
  zoom
42
44
  } : void 0;
43
45
  };
46
+ const calendarText = (view) => `${view.scale},${view.day}`;
47
+ const isDay = (text) => {
48
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
49
+ if (!match) return false;
50
+ const [year, month, day] = match.slice(1).map(Number);
51
+ const date = new Date(Date.UTC(year, month - 1, day));
52
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
53
+ };
54
+ const readCalendar = (text) => {
55
+ const parts = text?.split(",");
56
+ if (parts?.length !== 2) return void 0;
57
+ const [scale, day] = parts;
58
+ return (scale === "month" || scale === "week") && isDay(day) ? {
59
+ scale,
60
+ day
61
+ } : void 0;
62
+ };
44
63
  const listQuery = (address) => {
45
64
  const query = new URLSearchParams();
46
65
  for (const name of listWords) {
@@ -48,6 +67,7 @@ const listQuery = (address) => {
48
67
  if (value !== void 0 && value !== "") query.set(addressWords[name], value);
49
68
  }
50
69
  if (address.map) query.set(addressWords.map, mapText(address.map));
70
+ if (address.calendar) query.set(addressWords.calendar, calendarText(address.calendar));
51
71
  const text = query.toString();
52
72
  return text ? `?${text}` : "";
53
73
  };
@@ -107,6 +127,7 @@ const readAppPath = (pathAndQuery, pages) => {
107
127
  const orderBy = params.get(addressWords.orderBy) || void 0;
108
128
  const layout = filterLayout(params.get(addressWords.filterLayout));
109
129
  const map = readMap(params.get(addressWords.map));
130
+ const calendar = readCalendar(params.get(addressWords.calendar));
110
131
  const savedSearch = params.get(addressWords.savedSearch) || void 0;
111
132
  return {
112
133
  kind: "list",
@@ -116,6 +137,7 @@ const readAppPath = (pathAndQuery, pages) => {
116
137
  ...orderBy && { orderBy },
117
138
  ...layout && { filterLayout: layout },
118
139
  ...map && { map },
140
+ ...calendar && { calendar },
119
141
  ...savedSearch && { savedSearch }
120
142
  };
121
143
  };
package/dist/field.d.ts CHANGED
@@ -57,6 +57,11 @@ export type FieldMeta = {
57
57
  };
58
58
  /** A file field's settings (`f.file()`). */
59
59
  file?: FileOptions;
60
+ /** How a job fills it (`.ai()`). */
61
+ ai?: {
62
+ /** The prompt the job fills it from. */
63
+ prompt: string;
64
+ };
60
65
  };
61
66
  /** A field type: its name, the constraints it takes and how it builds the schema that checks a value. */
62
67
  export type FieldTypeDef = {
@@ -115,6 +120,13 @@ export declare class Field<V> {
115
120
  min(value: number): Field<V>;
116
121
  /** The greatest value of an integer, or the longest length of a text. Only `f.integer()` and `f.text()` take it. */
117
122
  max(value: number): Field<V>;
123
+ /**
124
+ * A job fills the field from `prompt`, `{field}` replaced by the record's values, after a create, and after an update
125
+ * that changes a field the prompt names, as the user who wrote; see [AI fields and actions](website/src/content/docs/reference/ai.mdx).
126
+ */
127
+ ai(o: {
128
+ prompt: string;
129
+ }): Field<V>;
118
130
  /** A copy of the field with `patch` applied, which every option returns. */
119
131
  protected with(patch: Partial<FieldMeta>): Field<V>;
120
132
  private constrain;
package/dist/field.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { promptError } from "./ai-prompt.js";
1
2
  import { z } from "zod";
2
3
  //#region src/field.ts
3
4
  const fieldRegistry = z.registry();
@@ -85,6 +86,15 @@ var Field = class Field {
85
86
  max(value) {
86
87
  return this.constrain("max", { max: value });
87
88
  }
89
+ /**
90
+ * A job fills the field from `prompt`, `{field}` replaced by the record's values, after a create, and after an update
91
+ * that changes a field the prompt names, as the user who wrote; see [AI fields and actions](website/src/content/docs/reference/ai.mdx).
92
+ */
93
+ ai(o) {
94
+ const error = promptError(o.prompt);
95
+ if (error) throw new Error(`.ai(): ${error}`);
96
+ return this.with({ ai: { prompt: o.prompt } });
97
+ }
88
98
  /** A copy of the field with `patch` applied, which every option returns. */
89
99
  with(patch) {
90
100
  return new Field(this.def, {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FileOptions, FileProcessor, FileProcessorInput, ImageFormat, ImageVariant, ImageVariantOptions, imageFormats } from "./files/file-options.js";
2
- import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, FormModel, GalleryModel, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
2
+ import { ActionInputKind, ActionInputModel, ActionModel, ActionRun, AppAddress, BadgeShape, BottomBarItemModel, BottomBarModel, CalendarView, CardFavoriteModel, CardPhotosModel, ChartGranularity, ChartModel, ChartRange, CheckedFilter, CommentsSectionModel, CompareOp, CreateModel, CustomSectionModel, DatePreset, DurationLiteral, DurationUnit, EventModel, EventOperation, ExportModel, FieldFormat, FieldModel, FieldPath, FieldType, FieldViewModel, FieldWidgetModel, FileFieldModel, FilterError, FilterErrorCode, FilterExpr, FilterLayout, FilterOf, FilterValue, FilterWidgetModel, FormModel, GalleryModel, GroupChartModel, HelpWhen, HistorySectionModel, JsonValue, KeyValue, LayoutItemModel, ListDisplayModel, ListModel, ListQuery, Literal, MapSectionModel, MapView, NavModel, NavRecentModel, NowValue, OrderByItem, PeriodChartModel, RecordMode, RelatedColumnModel, RelatedModel, ResourceModel, SaveFeedback, SaveSearchModel, SearchMatch, SearchResultModel, SearchesOfModel, SectionPresentation, ShowWhen, SignInPromptModel, SignInViewModel, SortSpec, Span, StatusTone, UserMenuItemModel, UserMenuModel, ViewModel } from "./model.js";
3
3
  import { AccessAction, AccessContext, AccessFn, AccessResult, AccessUser, FieldAccess, FieldRule } from "./access/types.js";
4
4
  import { AnyField, Constraint, Field, FieldMeta, FieldTypeDef } from "./field.js";
5
5
  import { RelationField } from "./relation-field.js";
@@ -41,4 +41,4 @@ import { AccessSource, ResolveOptions, ResolvedAccess, RowFilterOperation, acces
41
41
  import { restrictModel } from "./access/restrict.js";
42
42
  import "./access/index.js";
43
43
  import { AssistantAction, AssistantActionRequest, AssistantCardPart, AssistantDiff, AssistantEvent, AssistantMessage, AssistantMessageRequest, AssistantPart, AssistantState, AssistantStep, AssistantStepState, AssistantTablePart, AssistantTextPart, AssistantTone, AssistantWidgetPart } from "./assistant/protocol.js";
44
- export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type FormAddress, type FormInput, type FormModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
44
+ export { type Access, type AccessAction, type AccessContext, type AccessFn, type AccessResult, type AccessSource, type AccessUser, type ActionInputKind, type ActionInputModel, type ActionModel, type ActionRun, type AnyField, type AppAddress, type AssistantAction, type AssistantActionRequest, type AssistantCardPart, type AssistantDiff, type AssistantEvent, type AssistantMessage, type AssistantMessageRequest, type AssistantPart, type AssistantState, type AssistantStep, type AssistantStepState, type AssistantTablePart, type AssistantTextPart, type AssistantTone, type AssistantWidgetPart, type BadgeShape, type BottomBarItemModel, type BottomBarModel, type BottomBarNames, type BottomBarSource, type CalendarView, type Capability, type CapabilityAction, type CapabilityLimit, type CapabilityScope, type CardFavoriteModel, type CardPhotosModel, type ChartGranularity, type ChartInput, type ChartModel, type ChartRange, type CheckResult, type CheckedFilter, type CommentsSectionModel, type CompareOp, type ComponentRef, type Constraint, type CreateModel, type CreatedOptions, type CustomSectionModel, type DatePreset, type DeletedOptions, type DurationLiteral, type DurationUnit, type EvaluateOptions, type EventActor, type EventContext, type EventHelpers, type EventMatch, type EventModel, type EventOperation, type EventSpec, type ExportModel, type Field, type FieldAccess, type FieldFormat, type FieldMeta, type FieldModel, type FieldPath, type FieldRefValue, type FieldRefs, type FieldRule, type FieldType, type FieldTypeDef, type FieldView, type FieldViewModel, type FieldViews, type FieldWidgetModel, type FieldsInput, type FileField, type FileFieldModel, type FileOptions, type FileProcessor, type FileProcessorInput, type FilterError, type FilterErrorCode, type FilterExpr, type FilterLayout, type FilterLimits, type FilterOf, type FilterParse, type FilterValue, type FilterWidgetModel, type FormAddress, type FormInput, type FormModel, type GalleryModel, type Grant, type GroupChartInput, type GroupChartModel, type HeldCapability, type HelpWhen, type HistorySectionModel, type ImageFormat, type ImageVariant, type ImageVariantOptions, type InferRecord, type Issue, type JsonValue, type KeyOf, type KeyRefs, type KeyType, type KeyValue, type LayoutItemModel, type ListColumn, type ListDisplayModel, type ListModel, type ListQuery, type Literal, type MapSectionModel, type MapView, type NavModel, type NavRecentModel, type NowValue, type OrderByCheck, type OrderByItem, type OrderByParse, type PeriodChartInput, type PeriodChartModel, type RecordKey, type RecordMode, type RecordOf, type Ref, type Refs, type RelatedColumnModel, type RelatedModel, type RelationField, type ResolveOptions, type ResolvedAccess, type ResourceBuilder, type ResourceModel, type RoleEntry, type RoleLimit, type Roles, type RolesOptions, type RowFilterOperation, type Rule, type SaveFeedback, type SaveSearchModel, type SearchMatch, type SearchRef, type SearchRefs, type SearchResultModel, type SearchesOfModel, type SectionPresentation, type ShowWhen, type SignInAddress, type SignInPromptModel, type SignInStep, type SignInViewModel, type SortSpec, type Span, type StatusTone, type UndeletedOptions, type UpdatedOptions, type UserMenuItemModel, type UserMenuModel, type UserMenuSource, type Validator, type ViewBuilder, type ViewModel, type ViewProps, accessWarnings, addressWords, anonymousRole, appPath, bottomBar, bottomBarErrors, checkFilter, checkOrderBy, checkRecord, createWhere, defineRoles, enumLabel, evaluateFilter, f, fieldRef, fieldsUsedBy, fitsKeyType, formPath, formRoute, imageFormats, keyTypes, layout as l, maxBottomBarItems, parseFilter, parseOrderBy, pickView, printFilter, printOrderBy, readAppPath, readFormAddress, readSignInAddress, resolveAccess, resource, restrictModel, rule, signInParams, signInPath, signInRoute, signInSteps, userMenu, view, where };
@@ -1,7 +1,7 @@
1
1
  import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
2
2
  import { actionInputErrors, shownInputs } from "./action-inputs.js";
3
3
  import { chartFields, isGroupChart } from "./view-chart.js";
4
- import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
4
+ import { boardErrors, calendarErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
5
5
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
6
6
  import { bottomBarFor } from "./bottom-bar.js";
7
7
  import { decodeKey, encodeKey } from "./keys.js";
@@ -13,7 +13,8 @@ import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.j
13
13
  import { PluginHost, PluginRequirement, inRange, pluginProblem } from "./plugin-version.js";
14
14
  import { redactSecrets } from "./redact-secrets.js";
15
15
  import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
16
+ import { promptError, promptFields, renderPrompt } from "./ai-prompt.js";
16
17
  import { AddressCheck, addressErrors } from "./address-errors.js";
17
18
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
18
19
  import { readEventStream } from "./assistant/event-stream.js";
19
- export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
20
+ export { type AddressCheck, type PluginHost, type PluginRequirement, type PropShapes, type RefShape, type SearchTerm, actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/internal.js CHANGED
@@ -1,12 +1,13 @@
1
+ import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
2
+ import { promptError, promptFields, renderPrompt } from "./ai-prompt.js";
1
3
  import { checkedAccept, checkedProvider, parseSize } from "./files/file-options.js";
2
4
  import { matchedSearchField, matchesSearch, searchTerms, termMatches } from "./search-match.js";
3
5
  import { checkEventFilter } from "./events/event-models.js";
4
6
  import { defaultRecentLimit, navRecentErrors, statusTones } from "./nav-recent.js";
5
7
  import { actionInputErrors, shownInputs } from "./action-inputs.js";
6
- import { addressPlaceholders, appHrefError, fillAddress, hrefField, hrefPlaceholders, leavesApp, linkActionError } from "./address-template.js";
7
8
  import { chartFields, isGroupChart } from "./view-chart.js";
8
9
  import { fieldRefsIn, isFieldRef, markFieldRefs, unmarkFieldRefs } from "./field-ref.js";
9
- import { boardErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
10
+ import { boardErrors, calendarErrors, cardPhotoLimit, displayFields, tableDisplay } from "./view-displays.js";
10
11
  import { bottomBarFor } from "./bottom-bar.js";
11
12
  import { decodeKey, encodeKey } from "./keys.js";
12
13
  import { webhookResources } from "./webhooks/webhook-resources.js";
@@ -16,4 +17,4 @@ import { redactSecrets } from "./redact-secrets.js";
16
17
  import { addressErrors } from "./address-errors.js";
17
18
  import { applyAssistantEvent, emptyAssistantState } from "./assistant/apply-event.js";
18
19
  import { readEventStream } from "./assistant/event-stream.js";
19
- export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, pruneByShape, readEventStream, redactSecrets, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
20
+ export { actionInputErrors, addressErrors, addressPlaceholders, appHrefError, applyAssistantEvent, boardErrors, bottomBarFor, calendarErrors, cardPhotoLimit, chartFields, checkEventFilter, checkedAccept, checkedProvider, decodeKey, defaultRecentLimit, displayFields, emptyAssistantState, encodeKey, fieldRefsIn, fillAddress, hrefField, hrefPlaceholders, inRange, isFieldRef, isGroupChart, leavesApp, linkActionError, markFieldRefs, matchedSearchField, matchesSearch, navRecentErrors, parseSize, pluginProblem, promptError, promptFields, pruneByShape, readEventStream, redactSecrets, renderPrompt, searchTerms, shapes, shownInputs, statusTones, tableDisplay, termMatches, unmarkFieldRefs, webhookResources };
package/dist/layout.d.ts CHANGED
@@ -73,6 +73,14 @@ export declare const layout: {
73
73
  limit?: number;
74
74
  when?: ShowWhen;
75
75
  }) => LayoutItemModel;
76
+ /**
77
+ * The record's comments, oldest first, with who wrote each and when, and a box to write one; `@` mentions someone who
78
+ * may read the record. Needs `.comments()` on the resource; a view without a layout shows it last by itself. See
79
+ * [Record comments](website/src/content/docs/reference/comments.mdx).
80
+ */
81
+ comments: (title?: string, o?: {
82
+ when?: ShowWhen;
83
+ }) => LayoutItemModel;
76
84
  /**
77
85
  * A component of the project, registered in `protobase.ui.tsx` under its name, drawn where it is listed: in the main
78
86
  * column, or in the sidebar with `sidebar: true`. The record page stays around it, with its title, breadcrumbs,
package/dist/layout.js CHANGED
@@ -94,6 +94,16 @@ const layout = {
94
94
  };
95
95
  },
96
96
  /**
97
+ * The record's comments, oldest first, with who wrote each and when, and a box to write one; `@` mentions someone who
98
+ * may read the record. Needs `.comments()` on the resource; a view without a layout shows it last by itself. See
99
+ * [Record comments](website/src/content/docs/reference/comments.mdx).
100
+ */
101
+ comments: (title = "Comments", o = {}) => ({
102
+ kind: "comments",
103
+ title,
104
+ ...o.when !== void 0 && { when: o.when }
105
+ }),
106
+ /**
97
107
  * A component of the project, registered in `protobase.ui.tsx` under its name, drawn where it is listed: in the main
98
108
  * column, or in the sidebar with `sidebar: true`. The record page stays around it, with its title, breadcrumbs,
99
109
  * actions and save; the component reads the record with `useLayoutRecord()`.
package/dist/model.d.ts CHANGED
@@ -44,6 +44,11 @@ export type FieldModel = {
44
44
  };
45
45
  /** A file field's allowed types (empty: any), largest upload in bytes, provider, and what each upload writes. */
46
46
  file?: FileFieldModel;
47
+ /** A job fills it from the prompt after a create or an update of a field the prompt names (`.ai()`). */
48
+ ai?: {
49
+ /** The prompt, with `{field}` placeholders. */
50
+ prompt: string;
51
+ };
47
52
  };
48
53
  /** A file field's settings, as `/meta` gives them; see [Files](website/src/content/docs/reference/files.mdx). */
49
54
  export type FileFieldModel = {
@@ -105,6 +110,8 @@ export type ResourceModel = {
105
110
  events?: EventModel[];
106
111
  /** Whether every change of its records is kept, with who made it (`.history()`). */
107
112
  history?: true;
113
+ /** Whether people who may read its records comment on them (`.comments()`). */
114
+ comments?: true;
108
115
  };
109
116
  /** What happened to a record: `delete` and `undelete` include setting and clearing a soft delete field. */
110
117
  export type EventOperation = 'create' | 'update' | 'delete' | 'undelete';
@@ -164,11 +171,18 @@ export type MapView = {
164
171
  /** MapLibre's zoom, from 0 to 22. */
165
172
  zoom: number;
166
173
  };
174
+ /** Which month or week a list's calendar display shows: the grid's `scale`, and a UTC day (`YYYY-MM-DD`) in it. */
175
+ export type CalendarView = {
176
+ /** A month of whole weeks, or one week. */
177
+ scale: 'month' | 'week';
178
+ /** A day in the month or week, as `YYYY-MM-DD`. */
179
+ day: string;
180
+ };
167
181
  /**
168
182
  * A place in the app: a resource's list, one of its records, its create form, or a composed page. A record's `key` is
169
183
  * its one value or its parts, as the record API takes them; `readAppPath` gives it back as the API's text form. A create
170
184
  * form's `values` fill in fields of the new record by name, as text (`{ listing_id: '42' }`). A list's `map` is where its
171
- * map display looks. See [Addresses](website/src/content/docs/reference/layouts.md#addresses).
185
+ * map display looks, and its `calendar` the month or week its calendar display shows. See [Addresses](website/src/content/docs/reference/layouts.md#addresses).
172
186
  */
173
187
  export type AppAddress = {
174
188
  /** A resource's list. */
@@ -185,6 +199,8 @@ export type AppAddress = {
185
199
  filterLayout?: FilterLayout;
186
200
  /** Where its map display looks. */
187
201
  map?: MapView;
202
+ /** The month or week its calendar display shows. */
203
+ calendar?: CalendarView;
188
204
  /**
189
205
  * The saved search the list was opened from, by its key in the text form the record API takes: while the list's
190
206
  * filter differs from the search's, the list offers to update the search.
@@ -216,10 +232,11 @@ export type AppAddress = {
216
232
  page: string;
217
233
  };
218
234
  /**
219
- * How a value is shown: `badge` as a coloured badge, `code` in monospace, `percent` with a % sign. `relative`,
235
+ * How a value is shown: `badge` as a coloured badge, `code` in monospace, `percent` with a % sign; a text field's
236
+ * `email`, `phone` and `url` as a link that writes, calls or opens it, edited with the keyboard for it. `relative`,
220
237
  * `absolute` and `compact` are carried in `/meta` for apps that render their own lists.
221
238
  */
222
- export type FieldFormat = 'relative' | 'absolute' | 'compact' | 'percent' | 'badge' | 'code';
239
+ export type FieldFormat = 'relative' | 'absolute' | 'compact' | 'percent' | 'badge' | 'code' | 'email' | 'phone' | 'url';
223
240
  /** A view's hints for one field (`.fields((r) => ...)`); see [Names and columns](website/src/content/docs/reference/ui-config.mdx#names-and-columns). */
224
241
  export type FieldViewModel = {
225
242
  /** What the field is called, instead of its name in words. */
@@ -365,6 +382,21 @@ export type ListDisplayModel = {
365
382
  badge?: string;
366
383
  /** A few small fields, such as dates, each shown when it has a value. */
367
384
  meta?: string[];
385
+ } | {
386
+ /** The records on the days they cover, in a month grid or a week grid, moved to another day by dragging. */
387
+ kind: 'calendar';
388
+ /** What the address names (`?display=calendar`). */
389
+ name: string;
390
+ /** The display's name in the switch. */
391
+ label?: string;
392
+ /** The display's icon in the switch. */
393
+ icon?: string;
394
+ /** The filterable date or timestamp field a record starts on. */
395
+ start: string;
396
+ /** The filterable field of `start`'s type a record ends on; a record is on its start day alone when omitted. */
397
+ end?: string;
398
+ /** The event's text; the view's title when omitted. */
399
+ title?: string;
368
400
  } | {
369
401
  /** The rows drawn by a project component. */
370
402
  kind: 'custom';
@@ -597,6 +629,11 @@ export type RelatedModel = {
597
629
  sort?: string;
598
630
  /** Fields of `resource`, through relations too (`unitId.propertyId`). */
599
631
  columns?: string[];
632
+ /**
633
+ * The relation field of `resource` whose record a row opens, for rows that are links between this record and another
634
+ * (a join resource); by default the one relation in `resource`'s primary key besides `field`, when there is exactly one.
635
+ */
636
+ open?: string;
600
637
  /** How many records it shows, the first in `sort` order; with more, it says so ("The first 10 of 37.") beside "View all", which opens the rest in the list. */
601
638
  limit?: number;
602
639
  /** The display of `resource`'s list it opens in, such as `cards`; the list's default when omitted. */
@@ -659,6 +696,18 @@ export type HistorySectionModel = {
659
696
  /** Whether it shows on the form, the read view or both. */
660
697
  when?: ShowWhen;
661
698
  };
699
+ /**
700
+ * The record's comments, oldest first, each with its author and time, and a box to write a new one that mentions people
701
+ * with `@`. Needs `.comments()` on the resource. See [Record comments](website/src/content/docs/reference/comments.mdx).
702
+ */
703
+ export type CommentsSectionModel = {
704
+ /** A comments section. */
705
+ kind: 'comments';
706
+ /** The section's title. */
707
+ title: string;
708
+ /** Whether it shows on the form, the read view or both. */
709
+ when?: ShowWhen;
710
+ };
662
711
  /** How a section shows its fields on the read view: `facts` as a row of key facts; label/value rows when omitted. */
663
712
  export type SectionPresentation = 'facts';
664
713
  /** An item of the record page (`.layout((r) => ...)` with `l`); see [The record page](website/src/content/docs/reference/ui-config.mdx#the-record-page). */
@@ -687,7 +736,7 @@ export type LayoutItemModel = {
687
736
  fields: string[];
688
737
  /** Whether it shows on the form, the read view or both. */
689
738
  when?: ShowWhen;
690
- } | RelatedModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel;
739
+ } | RelatedModel | CustomSectionModel | MapSectionModel | GalleryModel | HistorySectionModel | CommentsSectionModel;
691
740
  /**
692
741
  * A record's photos as tiles in the main column under `title`, one large and the next few smaller, opening a lightbox
693
742
  * that steps through them all: first the record's own file field `image`, then the `image` of the records of
@@ -744,8 +793,8 @@ export type RecordMode = 'edit' | 'read' | 'locked';
744
793
  export type SaveFeedback = 'toast' | 'button';
745
794
  /**
746
795
  * What a named action does in the app: set fields of the record (`update`), delete it (`delete`), go to an address in
747
- * the app (`link` with `to`) or open a link that leaves it (`link` with `href`), `{field}` replaced by the record's
748
- * values. Without it the app runs a handler it registered by name.
796
+ * the app (`link` with `to`), open a link that leaves it (`link` with `href`), `{field}` replaced by the record's
797
+ * values, or have the model write a field (`ai`). Without it the app runs a handler it registered by name.
749
798
  */
750
799
  export type ActionRun = {
751
800
  /** Updates the record. */
@@ -768,6 +817,13 @@ export type ActionRun = {
768
817
  * the `https:` or `http:` URL the record holds, without which the action does not show.
769
818
  */
770
819
  href: string;
820
+ } | {
821
+ /** Has the model write one field of the record from `prompt`, `{field}` replaced by the record's values, as the user. */
822
+ kind: 'ai';
823
+ /** The prompt, with `{field}` placeholders; `/meta` leaves it out. */
824
+ prompt?: string;
825
+ /** The field it writes. */
826
+ writes: string;
771
827
  };
772
828
  /** A named action of a view (`.actions((a) => ...)`); see [Named actions](website/src/content/docs/reference/layouts.md#named-actions). */
773
829
  export type ActionModel = {
@@ -18,5 +18,6 @@ export type ResourceState = {
18
18
  };
19
19
  events?: EventSpec[];
20
20
  history?: boolean;
21
+ comments?: boolean;
21
22
  };
22
23
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { promptFields } from "./ai-prompt.js";
1
2
  import { buildEventModels } from "./events/event-models.js";
2
3
  import { snakeCase } from "./naming.js";
3
4
  //#region src/resource-model.ts
@@ -40,7 +41,8 @@ const fieldModel = (name, field) => {
40
41
  ...meta.file.variants && Object.keys(meta.file.variants).length > 0 && { variants: structuredClone(meta.file.variants) },
41
42
  ...meta.file.original && { original: { ...meta.file.original } },
42
43
  ...meta.file.external && { external: true }
43
- } }
44
+ } },
45
+ ...meta.ai && { ai: { prompt: meta.ai.prompt } }
44
46
  };
45
47
  };
46
48
  const assembleModel = (state) => {
@@ -59,6 +61,7 @@ const assembleModel = (state) => {
59
61
  ...state.searchMatch && Object.keys(state.searchMatch).length > 0 && { searchMatch: { ...state.searchMatch } },
60
62
  ...state.owner && { owner: state.owner },
61
63
  ...state.history && { history: true },
64
+ ...state.comments && { comments: true },
62
65
  ...state.position && { position: { ...state.position } }
63
66
  };
64
67
  };
@@ -118,6 +121,19 @@ const positionProblems = (model) => {
118
121
  ...parent && !parent.filterable ? [`position "${name}" is within "${within}", which is not filterable, and each parent's records are found by it`] : []
119
122
  ];
120
123
  };
124
+ const aiProblems = (model) => Object.values(model.fields).filter((field) => field.ai).flatMap((field) => [
125
+ ...field.type === "text" || field.type === "enum" ? [] : [`is ${field.type}, not text or an enum`],
126
+ ...field.nullable ? [] : ["must be optional(), empty until the job fills it"],
127
+ ...field.readOnly ? ["cannot be readOnly(), as the job writes it through the API"] : [],
128
+ ...field.sensitive ? ["cannot be sensitive"] : [],
129
+ ...promptFields(field.ai.prompt).flatMap((name) => {
130
+ const read = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
131
+ if (!read) return [`reads "${name}", which is not a field`];
132
+ if (name === field.name) return [`reads "${name}", itself`];
133
+ if (read.ai) return [`reads "${name}", which is an AI field too`];
134
+ return read.sensitive ? [`reads "${name}", which is sensitive`] : [];
135
+ })
136
+ ].map((problem) => `AI field "${field.name}" ${problem}`));
121
137
  const buildResourceModel = (state) => {
122
138
  const model = assembleModel(state);
123
139
  const events = state.events && buildEventModels(model, state.events);
@@ -125,6 +141,7 @@ const buildResourceModel = (state) => {
125
141
  ...sensitiveProblems(model),
126
142
  ...fileProblems(model),
127
143
  ...positionProblems(model),
144
+ ...aiProblems(model),
128
145
  ...events?.problems ?? []
129
146
  ];
130
147
  if (problems.length > 0) throw new Error(`Resource "${model.name}": ${problems.join("; ")}`);
@@ -103,6 +103,12 @@ export declare class ResourceBuilder<F extends FieldsInput = {}, K = never> {
103
103
  * [Record history](website/src/content/docs/reference/history.mdx).
104
104
  */
105
105
  history(): ResourceBuilder<F, K>;
106
+ /**
107
+ * Lets the people who may read a record comment on it, and mention others who may read it too, who get an email. The
108
+ * record page shows the thread with `l.comments()`, and admins list every comment as Comments. See
109
+ * [Record comments](website/src/content/docs/reference/comments.mdx).
110
+ */
111
+ comments(): ResourceBuilder<F, K>;
106
112
  /** The code predicates (`match`) of its events, by event name. */
107
113
  eventMatches(): Record<string, EventMatch<any>>;
108
114
  /**
package/dist/resource.js CHANGED
@@ -120,6 +120,14 @@ var ResourceBuilder = class ResourceBuilder {
120
120
  history() {
121
121
  return this.next({ history: true });
122
122
  }
123
+ /**
124
+ * Lets the people who may read a record comment on it, and mention others who may read it too, who get an email. The
125
+ * record page shows the thread with `l.comments()`, and admins list every comment as Comments. See
126
+ * [Record comments](website/src/content/docs/reference/comments.mdx).
127
+ */
128
+ comments() {
129
+ return this.next({ comments: true });
130
+ }
123
131
  /** The code predicates (`match`) of its events, by event name. */
124
132
  eventMatches() {
125
133
  return eventMatches(this.toModel(), this.state.events ?? []);
@@ -38,5 +38,9 @@ export declare const actions: {
38
38
  } | {
39
39
  href: string;
40
40
  })) => ActionModel;
41
+ ai: (name: string, o: ActionOptions & {
42
+ prompt: string;
43
+ writes: string;
44
+ }) => ActionModel;
41
45
  };
42
46
  //#endregion
@@ -1,5 +1,6 @@
1
- import { actionInputErrors, inputs } from "./action-inputs.js";
2
1
  import { linkActionError } from "./address-template.js";
2
+ import { promptError } from "./ai-prompt.js";
3
+ import { actionInputErrors, inputs } from "./action-inputs.js";
3
4
  //#region src/view-actions.ts
4
5
  const named = (name, o) => ({
5
6
  name,
@@ -64,6 +65,18 @@ const actions = {
64
65
  href: o.href
65
66
  }
66
67
  };
68
+ },
69
+ ai: (name, o) => {
70
+ const error = promptError(o.prompt) ?? (o.writes === "" ? "it writes no field" : void 0);
71
+ if (error) throw new Error(`Action "${name}": ${error}`);
72
+ return {
73
+ ...named(name, o),
74
+ run: {
75
+ kind: "ai",
76
+ prompt: o.prompt,
77
+ writes: o.writes
78
+ }
79
+ };
67
80
  }
68
81
  };
69
82
  //#endregion
@@ -92,6 +92,21 @@ export declare const displays: {
92
92
  badge?: Ref;
93
93
  meta?: Ref[];
94
94
  }, o?: Naming) => ListDisplayModel;
95
+ /**
96
+ * The records on the days from `start` to `end` (or on their start day alone), in a month grid of whole weeks or a
97
+ * week grid, with weeks from Monday and days in UTC. It loads only the days shown through the list API, with the
98
+ * list's filter and search, in start order. An event shows `title` (default: the view's title), after its time for a
99
+ * timestamp, and opens the record. Dragging an event to another day, or picking it up with Space and moving it with
100
+ * the arrow keys, saves the record with `start` and `end` moved by the same days, as one update with its ETag, so the
101
+ * resource's access rules and `validate` decide, and a refusal puts the event back and says why. `start` must be a
102
+ * filterable date or timestamp field and `end` a filterable field of the same type. Named `calendar` unless `name`
103
+ * says otherwise. See [Calendars](website/src/content/docs/reference/ui-config.mdx#calendars).
104
+ */
105
+ calendar: (fields: {
106
+ start: Ref;
107
+ end?: Ref;
108
+ title?: Ref;
109
+ }, o?: Naming) => ListDisplayModel;
95
110
  /**
96
111
  * The list drawn by a component of the project, registered in `protobase.ui.tsx` under its name. It gets `props` and
97
112
  * reads the rows with `useListDisplay()`; `fields` are the fields it shows, fetched with relation names.
@@ -107,6 +122,10 @@ export declare const displays: {
107
122
  export declare const boardErrors: (display: Extract<ListDisplayModel, {
108
123
  kind: "board";
109
124
  }>, model: ResourceModel) => string[];
110
- /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, and the field references in a custom display's props. */
125
+ /** What makes a calendar unable to place the records on days: a `start` that is no filterable date or timestamp, or an `end` that is no filterable field of its type. */
126
+ export declare const calendarErrors: (display: Extract<ListDisplayModel, {
127
+ kind: "calendar";
128
+ }>, model: ResourceModel) => string[];
129
+ /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, a calendar's dates, and the field references in a custom display's props. */
111
130
  export declare const displayFields: (display: ListDisplayModel) => string[];
112
131
  //#endregion
@@ -92,6 +92,24 @@ const displays = {
92
92
  ...fields.meta && { meta: fields.meta.map((ref) => ref.name) }
93
93
  }),
94
94
  /**
95
+ * The records on the days from `start` to `end` (or on their start day alone), in a month grid of whole weeks or a
96
+ * week grid, with weeks from Monday and days in UTC. It loads only the days shown through the list API, with the
97
+ * list's filter and search, in start order. An event shows `title` (default: the view's title), after its time for a
98
+ * timestamp, and opens the record. Dragging an event to another day, or picking it up with Space and moving it with
99
+ * the arrow keys, saves the record with `start` and `end` moved by the same days, as one update with its ETag, so the
100
+ * resource's access rules and `validate` decide, and a refusal puts the event back and says why. `start` must be a
101
+ * filterable date or timestamp field and `end` a filterable field of the same type. Named `calendar` unless `name`
102
+ * says otherwise. See [Calendars](website/src/content/docs/reference/ui-config.mdx#calendars).
103
+ */
104
+ calendar: (fields, o = {}) => ({
105
+ kind: "calendar",
106
+ name: o.name ?? "calendar",
107
+ ...naming(o),
108
+ start: fields.start.name,
109
+ ...fields.end && { end: fields.end.name },
110
+ ...fields.title && { title: fields.title.name }
111
+ }),
112
+ /**
95
113
  * The list drawn by a component of the project, registered in `protobase.ui.tsx` under its name. It gets `props` and
96
114
  * reads the rows with `useListDisplay()`; `fields` are the fields it shows, fetched with relation names.
97
115
  */
@@ -131,7 +149,21 @@ const boardErrors = (display, model) => {
131
149
  if (field.type !== "enum") return [`"${display.by}" on ${model.name} is ${field.type}, not an enum, whose values are the board's columns`];
132
150
  return field.filterable ? [] : [`"${display.by}" on ${model.name} is not filterable, and each column of the board filters by its value`];
133
151
  };
134
- /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, and the field references in a custom display's props. */
152
+ /** What makes a calendar unable to place the records on days: a `start` that is no filterable date or timestamp, or an `end` that is no filterable field of its type. */
153
+ const calendarErrors = (display, model) => {
154
+ const known = (name) => Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
155
+ const start = known(display.start);
156
+ const end = display.end === void 0 ? void 0 : known(display.end);
157
+ if (!start) return [];
158
+ if (start.type !== "date" && start.type !== "timestamp") return [`start "${display.start}" on ${model.name} is ${start.type}, not a date or timestamp`];
159
+ const unfilterable = (name) => `${name === display.start ? "start" : "end"} "${name}" on ${model.name} is not filterable, and the calendar filters by it for the days it shows`;
160
+ return [
161
+ ...start.filterable ? [] : [unfilterable(display.start)],
162
+ ...end && end.type !== start.type ? [`end "${display.end}" on ${model.name} is ${end.type}, not ${start.type} like start "${display.start}"`] : [],
163
+ ...end && end.type === start.type && !end.filterable ? [unfilterable(display.end)] : []
164
+ ];
165
+ };
166
+ /** Every field a display names: what it shows, a map's position and the fields saying how exact it is, a board's columns, a calendar's dates, and the field references in a custom display's props. */
135
167
  const displayFields = (display) => {
136
168
  if (display.kind === "map") return [
137
169
  display.latitude,
@@ -149,6 +181,11 @@ const displayFields = (display) => {
149
181
  display.badge,
150
182
  ...display.meta ?? []
151
183
  ].filter((name) => name !== void 0);
184
+ if (display.kind === "calendar") return [
185
+ display.start,
186
+ display.end,
187
+ display.title
188
+ ].filter((name) => name !== void 0);
152
189
  return [
153
190
  display.image,
154
191
  display.title,
@@ -161,4 +198,4 @@ const displayFields = (display) => {
161
198
  ].filter((name) => name !== void 0);
162
199
  };
163
200
  //#endregion
164
- export { boardErrors, cardPhotoLimit, customComponent, displayErrors, displayFields, displays, tableDisplay };
201
+ export { boardErrors, calendarErrors, cardPhotoLimit, customComponent, displayErrors, displayFields, displays, tableDisplay };
@@ -26,7 +26,10 @@ export declare class FieldView<N extends string = string, V = unknown> {
26
26
  * view and locked form; `always` when omitted. Its lists, cards and filters show it either way.
27
27
  */
28
28
  when(when: ShowWhen): FieldView<N, V>;
29
- /** How the value shows: `badge` for statuses, `code` in monospace, `percent` with a % sign. */
29
+ /**
30
+ * How the value shows: `badge` for statuses, `code` in monospace, `percent` with a % sign; for a text field, `email`,
31
+ * `phone` and `url` as a mail, call or open link, edited with an email, phone or address keyboard on a mobile screen.
32
+ */
30
33
  format(format: FieldFormat): FieldView<N, V>;
31
34
  /**
32
35
  * A text of several lines, such as a description: forms edit it in a box that grows with the text, and the record
@@ -30,7 +30,10 @@ var FieldView = class FieldView {
30
30
  when(when) {
31
31
  return this.with({ when });
32
32
  }
33
- /** How the value shows: `badge` for statuses, `code` in monospace, `percent` with a % sign. */
33
+ /**
34
+ * How the value shows: `badge` for statuses, `code` in monospace, `percent` with a % sign; for a text field, `email`,
35
+ * `phone` and `url` as a mail, call or open link, edited with an email, phone or address keyboard on a mobile screen.
36
+ */
34
37
  format(format) {
35
38
  return this.with({ format });
36
39
  }
package/dist/view.d.ts CHANGED
@@ -69,7 +69,7 @@ export declare class ViewBuilder<R extends {
69
69
  fields(build: (r: FieldViews<R>) => Record<string, FieldView>): ViewBuilder<R>;
70
70
  /**
71
71
  * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, the
72
- * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.custom`), one of which
72
+ * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.calendar`, `d.custom`), one of which
73
73
  * may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
74
74
  * editable field is not among the columns. See
75
75
  * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
package/dist/view.js CHANGED
@@ -5,7 +5,7 @@ import { actions } from "./view-actions.js";
5
5
  import { chartModel } from "./view-chart.js";
6
6
  import { formModel } from "./view-form.js";
7
7
  import { columns, isListColumn } from "./view-columns.js";
8
- import { boardErrors, displayErrors, displayFields, displays } from "./view-displays.js";
8
+ import { boardErrors, calendarErrors, displayErrors, displayFields, displays } from "./view-displays.js";
9
9
  import { FieldView } from "./view-field.js";
10
10
  import { widgets } from "./view-widgets.js";
11
11
  //#region src/view.ts
@@ -76,7 +76,7 @@ var ViewBuilder = class ViewBuilder {
76
76
  }
77
77
  /**
78
78
  * The list: its table's `columns`, fields or columns of related records (`c.related`), `sort` and `search`, the
79
- * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.custom`), one of which
79
+ * columns that are `editable` in their cell, and other `displays` to switch to (`d.cards`, `d.map`, `d.board`, `d.calendar`, `d.custom`), one of which
80
80
  * may be the `default` the list opens in. Throws when display or column names collide, `default` names none or an
81
81
  * editable field is not among the columns. See
82
82
  * [Editing in the list](website/src/content/docs/reference/ui-config.mdx#editing-in-the-list).
@@ -255,6 +255,8 @@ var ViewBuilder = class ViewBuilder {
255
255
  if (flag !== void 0) throw new Error(`View "${this.state.resource}": display "${display.name}" shows "${flag}" as a flag, which is not a boolean field`);
256
256
  const board = display.kind === "board" ? boardErrors(display, resource) : [];
257
257
  if (board.length > 0) throw new Error(`View "${this.state.resource}": display "${display.name}": ${board.join("; ")}`);
258
+ const calendar = display.kind === "calendar" ? calendarErrors(display, resource) : [];
259
+ if (calendar.length > 0) throw new Error(`View "${this.state.resource}": display "${display.name}": ${calendar.join("; ")}`);
258
260
  }
259
261
  const clash = resource ? list?.relatedColumns?.find((column) => Object.hasOwn(resource.fields, column.name)) : void 0;
260
262
  if (clash) throw new Error(`View "${this.state.resource}": related column "${clash.name}" has the name of a field of the resource`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/schema",
3
- "version": "0.21.7",
3
+ "version": "0.21.9",
4
4
  "description": "Resource, field, view and access definitions for Protobase configs.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {