@checkcourt/sdk 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -178,6 +178,21 @@ ui.form({
178
178
  });
179
179
  ```
180
180
 
181
+ `ui.field.date` and `ui.field.time` render native date and time pickers. `default`, `min` and
182
+ `max` are `YYYY-MM-DD` strings for dates and `HH:MM` (24-hour) strings for times; `time` takes an
183
+ optional `step` in minutes. Both submit their value as a string:
184
+
185
+ ```ts
186
+ ui.form({
187
+ actionId: "block",
188
+ submitLabel: "Sperren",
189
+ fields: [
190
+ ui.field.date("day", "Tag", { min: "2026-01-01", required: true }),
191
+ ui.field.time("from", "Von", { min: "07:00", max: "22:00", step: 30, required: true }),
192
+ ],
193
+ });
194
+ ```
195
+
181
196
  Compare `context.installation_id` and `context.tenant_id` with what you stored from
182
197
  `app.installed` before you act on a request. For the context token alone (for example in
183
198
  the backend of an iframe extension), use `verifyExtensionContext(token, secret)`.
@@ -192,7 +207,8 @@ with the matching builder; each throws when the answer would break CheckCourt's
192
207
  | `court.annotation` | `ext.date`, `ext.courts` | `ui.annotations([{ court_id, label, variant? }])`, label up to 24 characters, one per court |
193
208
  | `member.list.column` | `ext.members` | `ui.column({ title, values: [{ member_id, text, variant? }] })`, title up to 20, text up to 24 characters |
194
209
  | `booking.hint` | `ext.draft` | `ui.hint([...])` with up to 6 text, badge, key_value or link blocks; answer within 1 second |
195
- | `booking_plan.action`, `sidebar.action` | `booking_plan.action`: the day as subject | `ui.doc([...])`, shown in a dialog after a click; `ui.hidden()` closes it |
210
+ | `booking_plan.action` | `ext.date`, `ext.courts` (and the day as subject) | `ui.doc([...])`, shown in a dialog after a click; `ui.hidden()` closes it |
211
+ | `sidebar.action` | nothing extra | `ui.doc([...])`, shown in a dialog after a click; `ui.hidden()` closes it |
196
212
  | `member.settings.section` | nothing extra | `ui.doc([...])`, a card on the member's own settings page |
197
213
 
198
214
  ```ts
@@ -115,7 +115,7 @@ export type ExtensionContextClaims = {
115
115
  export declare function verifyExtensionContext(token: string, secret: string, options?: {
116
116
  now?: Date | number;
117
117
  }): Promise<ExtensionContextClaims>;
118
- /** A court of the plan a `court.annotation` request covers. */
118
+ /** A court of the plan a `court.annotation` or `booking_plan.action` request covers. */
119
119
  export interface AnnotationCourt {
120
120
  id: number;
121
121
  name: string;
@@ -145,9 +145,9 @@ export interface ExtensionRenderRequest {
145
145
  context: ExtensionContextClaims;
146
146
  point: ExtensionPoint;
147
147
  subject: ExtensionSubject | null;
148
- /** `court.annotation`: the plan's day, YYYY-MM-DD. */
148
+ /** `court.annotation` and `booking_plan.action`: the plan's day, YYYY-MM-DD. */
149
149
  date?: string;
150
- /** `court.annotation`: every court of the plan, answered in one document. */
150
+ /** `court.annotation` (one document for all) and `booking_plan.action`: every court of the plan. */
151
151
  courts?: AnnotationCourt[];
152
152
  /** `member.list.column`: the members on the visible page. */
153
153
  members?: ColumnMember[];
@@ -162,6 +162,10 @@ export interface ExtensionActionRequest {
162
162
  subject: ExtensionSubject | null;
163
163
  actionId: string;
164
164
  values: UiFormValues;
165
+ /** `booking_plan.action`: the plan's day, YYYY-MM-DD. */
166
+ date?: string;
167
+ /** `booking_plan.action`: every court of the plan, so a form can offer a court select. */
168
+ courts?: AnnotationCourt[];
165
169
  }
166
170
  export type ExtensionRequest = ExtensionRenderRequest | ExtensionActionRequest;
167
171
  type HeaderSource = Headers | Record<string, string | string[] | undefined>;
@@ -169,7 +173,8 @@ type HeaderSource = Headers | Record<string, string | string[] | undefined>;
169
173
  * Verifies a declarative extension POST: the `CheckCourt-Signature` over the raw body (it binds
170
174
  * `action_id` and `values` to the token), the context token, and that body, header token and
171
175
  * claims agree. Renders at `court.annotation`, `member.list.column` and `booking.hint` also
172
- * carry `date` and `courts`, `members` or `draft`. Throws `ExtensionVerificationError`.
176
+ * carry `date` and `courts`, `members` or `draft`; `booking_plan.action` renders and actions
177
+ * carry the plan's `date` and `courts`. Throws `ExtensionVerificationError`.
173
178
  */
174
179
  export declare function verifyExtensionRequest(options: {
175
180
  secret: string;
@@ -84,17 +84,21 @@ function sameSubject(a, b) {
84
84
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
85
85
  const TIME = /^\d{2}:\d{2}$/;
86
86
  const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
87
+ /** Parses the `date` and `courts` a court.annotation or booking_plan.action request carries. */
88
+ function planCourts(body) {
89
+ const { date, courts } = body;
90
+ if (typeof date !== "string" || !DATE.test(date))
91
+ throw fail("invalid_body", "date is not YYYY-MM-DD");
92
+ if (!Array.isArray(courts) || !courts.every((c) => isObject(c) && typeof c.id === "number" && typeof c.name === "string")) {
93
+ throw fail("invalid_body", "courts is not a list of { id, name }");
94
+ }
95
+ return { date, courts: courts.map((c) => ({ id: c.id, name: c.name })) };
96
+ }
87
97
  function surfaceFields(point, body) {
88
98
  switch (point) {
89
- case "court.annotation": {
90
- const { date, courts } = body;
91
- if (typeof date !== "string" || !DATE.test(date))
92
- throw fail("invalid_body", "date is not YYYY-MM-DD");
93
- if (!Array.isArray(courts) || !courts.every((c) => isObject(c) && typeof c.id === "number" && typeof c.name === "string")) {
94
- throw fail("invalid_body", "courts is not a list of { id, name }");
95
- }
96
- return { date, courts: courts.map((c) => ({ id: c.id, name: c.name })) };
97
- }
99
+ case "court.annotation":
100
+ case "booking_plan.action":
101
+ return planCourts(body);
98
102
  case "member.list.column": {
99
103
  const { members } = body;
100
104
  if (!Array.isArray(members) ||
@@ -134,7 +138,8 @@ function surfaceFields(point, body) {
134
138
  * Verifies a declarative extension POST: the `CheckCourt-Signature` over the raw body (it binds
135
139
  * `action_id` and `values` to the token), the context token, and that body, header token and
136
140
  * claims agree. Renders at `court.annotation`, `member.list.column` and `booking.hint` also
137
- * carry `date` and `courts`, `members` or `draft`. Throws `ExtensionVerificationError`.
141
+ * carry `date` and `courts`, `members` or `draft`; `booking_plan.action` renders and actions
142
+ * carry the plan's `date` and `courts`. Throws `ExtensionVerificationError`.
138
143
  */
139
144
  export async function verifyExtensionRequest(options) {
140
145
  try {
@@ -185,5 +190,6 @@ export async function verifyExtensionRequest(options) {
185
190
  subject,
186
191
  actionId: body.action_id,
187
192
  values: values,
193
+ ...(context.point === "booking_plan.action" ? planCourts(body) : {}),
188
194
  };
189
195
  }
package/dist/ui.d.ts CHANGED
@@ -93,7 +93,33 @@ export type UiSwitchField = {
93
93
  label: string;
94
94
  default?: boolean;
95
95
  };
96
- export type UiFormField = UiTextField | UiNumberField | UiSelectField | UiSwitchField;
96
+ export type UiDateField = {
97
+ type: "date";
98
+ name: string;
99
+ label: string;
100
+ /** YYYY-MM-DD. */
101
+ default?: string;
102
+ /** YYYY-MM-DD. */
103
+ min?: string;
104
+ /** YYYY-MM-DD. */
105
+ max?: string;
106
+ required?: boolean;
107
+ };
108
+ export type UiTimeField = {
109
+ type: "time";
110
+ name: string;
111
+ label: string;
112
+ /** HH:MM, 24-hour. */
113
+ default?: string;
114
+ /** HH:MM, 24-hour. */
115
+ min?: string;
116
+ /** HH:MM, 24-hour. */
117
+ max?: string;
118
+ required?: boolean;
119
+ /** Granularity of the time picker in minutes. */
120
+ step?: number;
121
+ };
122
+ export type UiFormField = UiTextField | UiNumberField | UiSelectField | UiSwitchField | UiDateField | UiTimeField;
97
123
  export type UiFormBlock = {
98
124
  type: "form";
99
125
  fields: UiFormField[];
@@ -263,6 +289,8 @@ export declare const ui: {
263
289
  number(name: string, label: string, options?: Opt<Omit<UiNumberField, "type" | "name" | "label">>): UiNumberField;
264
290
  select(name: string, label: string, options: UiSelectField["options"], extra?: Opt<Pick<UiSelectField, "default" | "required">>): UiSelectField;
265
291
  switch(name: string, label: string, options?: Opt<Pick<UiSwitchField, "default">>): UiSwitchField;
292
+ date(name: string, label: string, options?: Opt<Omit<UiDateField, "type" | "name" | "label">>): UiDateField;
293
+ time(name: string, label: string, options?: Opt<Omit<UiTimeField, "type" | "name" | "label">>): UiTimeField;
266
294
  };
267
295
  readonly divider: () => UiDividerBlock;
268
296
  readonly stack: (children: UiBlock[]) => UiStackBlock;
package/dist/ui.js CHANGED
@@ -40,6 +40,12 @@ const field = {
40
40
  switch(name, label, options = {}) {
41
41
  return { type: "switch", name, label, ...options };
42
42
  },
43
+ date(name, label, options = {}) {
44
+ return compact({ type: "date", name, label, ...options });
45
+ },
46
+ time(name, label, options = {}) {
47
+ return compact({ type: "time", name, label, ...options });
48
+ },
43
49
  };
44
50
  function compact(value) {
45
51
  return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkcourt/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Official TypeScript SDK for the CheckCourt app platform",
5
5
  "private": false,
6
6
  "license": "MIT",