@signalcore/ui 1.0.0-beta.3 → 1.0.0-beta.4

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 (71) hide show
  1. package/README.md +79 -0
  2. package/dist/AgentChat.d.ts +19 -3
  3. package/dist/AgentChat.js +149 -27
  4. package/dist/AgentChat.js.map +1 -1
  5. package/dist/AgentSpace.d.ts +14 -2
  6. package/dist/AgentSpace.js +3 -1
  7. package/dist/AgentSpace.js.map +1 -1
  8. package/dist/View.d.ts +48 -0
  9. package/dist/View.js +101 -0
  10. package/dist/View.js.map +1 -0
  11. package/dist/blocks.d.ts +83 -2
  12. package/dist/client.d.ts +9 -1
  13. package/dist/client.js +1 -0
  14. package/dist/client.js.map +1 -1
  15. package/dist/es.js +12 -0
  16. package/dist/es.js.map +1 -1
  17. package/dist/index.d.ts +11 -5
  18. package/dist/index.js +7 -1
  19. package/dist/kit/Agenda.d.ts +25 -0
  20. package/dist/kit/Agenda.js +119 -0
  21. package/dist/kit/Agenda.js.map +1 -0
  22. package/dist/kit/Combobox.js +1 -1
  23. package/dist/kit/DateField.js +1 -1
  24. package/dist/kit/Kanban.d.ts +17 -0
  25. package/dist/kit/Kanban.js +157 -0
  26. package/dist/kit/Kanban.js.map +1 -0
  27. package/dist/kit/TimeField.js +1 -1
  28. package/dist/kit/Timeline.d.ts +14 -0
  29. package/dist/kit/Timeline.js +71 -0
  30. package/dist/kit/Timeline.js.map +1 -0
  31. package/dist/kit/context.d.ts +1 -1
  32. package/dist/kit/context.js.map +1 -1
  33. package/dist/kit/messages.d.ts +22 -0
  34. package/dist/kit/messages.js +10 -0
  35. package/dist/kit/messages.js.map +1 -1
  36. package/dist/surface/Placed.d.ts +20 -0
  37. package/dist/surface/Placed.js +70 -0
  38. package/dist/surface/Placed.js.map +1 -0
  39. package/dist/surface/Surface.d.ts +4 -2
  40. package/dist/surface/Surface.js +78 -2
  41. package/dist/surface/Surface.js.map +1 -1
  42. package/dist/surface/places.d.ts +16 -0
  43. package/dist/surface/places.js +15 -0
  44. package/dist/surface/places.js.map +1 -0
  45. package/dist/surface/scope.d.ts +17 -1
  46. package/dist/surface/scope.js +1 -0
  47. package/dist/surface/scope.js.map +1 -1
  48. package/dist/types.d.ts +12 -0
  49. package/dist/types.js.map +1 -1
  50. package/dist/widget.js +17 -17
  51. package/package.json +1 -1
  52. package/src/AgentChat.tsx +215 -34
  53. package/src/AgentSpace.tsx +17 -1
  54. package/src/View.tsx +117 -0
  55. package/src/blocks.ts +100 -2
  56. package/src/client.ts +16 -0
  57. package/src/contract.ts +166 -5
  58. package/src/es.ts +9 -0
  59. package/src/index.ts +27 -2
  60. package/src/kit/Agenda.tsx +161 -0
  61. package/src/kit/Kanban.tsx +182 -0
  62. package/src/kit/Timeline.tsx +58 -0
  63. package/src/kit/context.tsx +45 -1
  64. package/src/kit/messages.ts +31 -0
  65. package/src/styles.css +568 -0
  66. package/src/surface/Placed.tsx +98 -0
  67. package/src/surface/Surface.tsx +92 -1
  68. package/src/surface/places.ts +19 -0
  69. package/src/surface/scope.ts +24 -1
  70. package/src/types.ts +13 -0
  71. package/src/widget/element.tsx +41 -3
package/src/client.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  FileLink,
14
14
  FilePreview,
15
15
  StoredFile,
16
+ ViewRendered,
16
17
  } from "./types";
17
18
 
18
19
  /**
@@ -153,6 +154,14 @@ export interface Client {
153
154
  /** One run of an automation with its record: every call it made, step by step. */
154
155
  get: (id: string) => Promise<RunRecord>;
155
156
  };
157
+ /** What an agent keeps on show for each person it serves (`compose_view`, `/agents/{id}/views`). */
158
+ views: {
159
+ /**
160
+ * A view rendered from the rows now: for a person, their own; for the business, for the
161
+ * contact it names (`contact`, their id).
162
+ */
163
+ render: (name: string, contact?: string) => Promise<ViewRendered>;
164
+ };
156
165
  /**
157
166
  * The tasks an agent runs for contacts, each with its newest runs: a person's own only (what
158
167
  * they asked the agent for in their conversations); the business's, every one.
@@ -366,6 +375,13 @@ export function createClient(options: ClientOptions): Client {
366
375
  runs: {
367
376
  get: (id) => json("GET", `/runs/${seg(id)}`),
368
377
  },
378
+ views: {
379
+ render: (name, contact) =>
380
+ json(
381
+ "GET",
382
+ `/views/${seg(name)}/render${contact === undefined ? "" : `?contact=${seg(contact)}`}`,
383
+ ),
384
+ },
369
385
  tasks: {
370
386
  list: (agentId) => json("GET", `/agents/${seg(agentId)}/tasks`),
371
387
  switch: (agentId, taskId, on) =>
package/src/contract.ts CHANGED
@@ -247,6 +247,20 @@ export interface AutomationOut {
247
247
  last_run: RunOut | null;
248
248
  }
249
249
 
250
+ /** What a host block is for, in a sentence the agent reads, and the JSON Schema of its props: an object's. */
251
+ export interface BlockIn {
252
+ description: string;
253
+ schema: Record<string, unknown>;
254
+ }
255
+
256
+ export interface BlockOut {
257
+ name: string;
258
+ description: string;
259
+ schema: Record<string, unknown>;
260
+ created_at: string;
261
+ updated_at: string;
262
+ }
263
+
250
264
  /** What the automation moves: its table's rows counted by the value of one field. */
251
265
  export interface Board {
252
266
  table: string;
@@ -255,6 +269,38 @@ export interface Board {
255
269
  counts: Array<Count>;
256
270
  }
257
271
 
272
+ /** Cards in columns, a column per stage. With `action`, the person may move a card to another column, which sends `action` with `{"card": id, "column": id}`: the agent then does what moving it means. */
273
+ export interface BoardBlock {
274
+ type: "board";
275
+ columns: Array<BoardColumn>;
276
+ action?: string | null;
277
+ }
278
+
279
+ /** One card of a board. */
280
+ export interface BoardCard {
281
+ id: string;
282
+ title: string;
283
+ subtitle?: string | null;
284
+ meta?: string | null;
285
+ }
286
+
287
+ export interface BoardColumn {
288
+ id: string;
289
+ title: string;
290
+ cards: Array<BoardCard>;
291
+ }
292
+
293
+ /** The rows as cards in columns, a column per option of a choice (`column`). */
294
+ export interface BoardPiece {
295
+ type: "board";
296
+ title: string;
297
+ item: string;
298
+ column: string;
299
+ subtitle?: string | null;
300
+ meta?: string | null;
301
+ query: Rows;
302
+ }
303
+
258
304
  /** One thing the person can do: pressing it sends `action`, or, with `url`, opens the page (a payment, a document). Two buttons that do different things carry different action names: the name is the whole of what comes back. */
259
305
  export interface Button {
260
306
  type: "button";
@@ -264,6 +310,38 @@ export interface Button {
264
310
  tone?: "primary" | "neutral" | "danger";
265
311
  }
266
312
 
313
+ /** A day's events, or a week's from `start`, at their hours. With `action`, pressing one sends it with `{"event": id}`; one with `url` opens its page instead. */
314
+ export interface Calendar {
315
+ type: "calendar";
316
+ span?: "day" | "week";
317
+ start: string;
318
+ events: Array<CalendarEvent>;
319
+ action?: string | null;
320
+ }
321
+
322
+ /** One thing on a calendar: its title at its time (a day alone: all of it), until `end`, with a line of detail (where, with whom) and a page to open (`url`). */
323
+ export interface CalendarEvent {
324
+ id: string;
325
+ title: string;
326
+ start: string;
327
+ end?: string | null;
328
+ detail?: string | null;
329
+ url?: string | null;
330
+ }
331
+
332
+ /** The rows whose date (`at`) falls today (`day`) or this week, at their hours: each called by `item`, until `end`, with `detail` under it and `link` (an https address) to open. */
333
+ export interface CalendarPiece {
334
+ type: "calendar";
335
+ title: string;
336
+ span?: "day" | "week";
337
+ item: string;
338
+ at: string;
339
+ end?: string | null;
340
+ detail?: string | null;
341
+ link?: string | null;
342
+ query: Scope;
343
+ }
344
+
267
345
  /** The call a conversation is (VOICE.md): the owner's in the browser or a phone call, when the worker took it and when it ended, why and how long. */
268
346
  export interface CallGlance {
269
347
  id: string;
@@ -290,7 +368,7 @@ export interface Card {
290
368
  title?: string | null;
291
369
  subtitle?: string | null;
292
370
  image?: string | null;
293
- children: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | AutomationBlock | FileBlock>;
371
+ children: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | Calendar | Timeline | BoardBlock | AutomationBlock | FileBlock | HostBlock>;
294
372
  }
295
373
 
296
374
  /** A page of the apps a search finds. */
@@ -603,7 +681,7 @@ export interface DashboardOut {
603
681
  export interface DashboardView {
604
682
  id: string;
605
683
  title: string;
606
- blocks: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | AutomationBlock | FileBlock>;
684
+ blocks: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | Calendar | Timeline | BoardBlock | AutomationBlock | FileBlock | HostBlock>;
607
685
  rendered_at: string;
608
686
  }
609
687
 
@@ -952,6 +1030,24 @@ export interface HolderIn {
952
1030
  title?: string | null;
953
1031
  }
954
1032
 
1033
+ /** One of the host's own blocks, by the name it declared: `props` as its schema says (the API checks them against it), and `text`, the same in words, which a page that doesn't draw it shows instead, as a channel does. */
1034
+ export interface HostBlock {
1035
+ type: "host";
1036
+ name: string;
1037
+ props: Record<string, unknown>;
1038
+ text: string;
1039
+ }
1040
+
1041
+ /** One of the host's own blocks: its `props` as given; with a rows `query`, one block per row, each prop of `fields` taken from that row's field. `text`: the same in words. */
1042
+ export interface HostPiece {
1043
+ type: "host";
1044
+ name: string;
1045
+ text: string;
1046
+ props?: Record<string, unknown>;
1047
+ fields?: Record<string, string>;
1048
+ query?: Rows | null;
1049
+ }
1050
+
955
1051
  /** The week, in the agent's time zone: each day's stretches, in order and apart; a day left out, or empty, is closed. */
956
1052
  export interface Hours {
957
1053
  mon?: Array<Span>;
@@ -1466,6 +1562,11 @@ export interface SchemasDecisionOut {
1466
1562
  run_id: string | null;
1467
1563
  }
1468
1564
 
1565
+ export interface Scope {
1566
+ table: string;
1567
+ where?: Array<Where>;
1568
+ }
1569
+
1469
1570
  export interface SecretIn {
1470
1571
  value: string;
1471
1572
  hosts?: Array<string>;
@@ -1548,7 +1649,7 @@ export interface Span {
1548
1649
  export interface Stack {
1549
1650
  type: "stack";
1550
1651
  direction?: "column" | "row";
1551
- children: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | AutomationBlock | FileBlock>;
1652
+ children: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | Calendar | Timeline | BoardBlock | AutomationBlock | FileBlock | HostBlock>;
1552
1653
  }
1553
1654
 
1554
1655
  /** One number that matters, under its label, with a line of context (a change, a goal). */
@@ -1729,6 +1830,33 @@ export interface TextPreview {
1729
1830
  truncated: boolean;
1730
1831
  }
1731
1832
 
1833
+ /** Dated things, one under another, the newest first. */
1834
+ export interface Timeline {
1835
+ type: "timeline";
1836
+ items: Array<TimelineItem>;
1837
+ }
1838
+
1839
+ /** One thing on a timeline: what happened (or will) and when, a line about it, and where it stands: `status` in a word, `tone` its mark. */
1840
+ export interface TimelineItem {
1841
+ id?: string | null;
1842
+ at: string;
1843
+ title: string;
1844
+ text?: string | null;
1845
+ status?: string | null;
1846
+ tone?: "info" | "success" | "warning" | "danger" | null;
1847
+ }
1848
+
1849
+ /** The rows by a date (`at`), the newest first: each called by `item`, with `text` under it and `status`, where it stands, in a word. */
1850
+ export interface TimelinePiece {
1851
+ type: "timeline";
1852
+ title: string;
1853
+ item: string;
1854
+ at: string;
1855
+ text?: string | null;
1856
+ status?: string | null;
1857
+ query: Rows;
1858
+ }
1859
+
1732
1860
  export interface TokenIn {
1733
1861
  agents: Array<string>;
1734
1862
  external_id: string;
@@ -1848,6 +1976,39 @@ export interface UsedOut {
1848
1976
  unpriced: number;
1849
1977
  }
1850
1978
 
1979
+ /** A view's name, title or blocks changed: only what is sent. */
1980
+ export interface ViewChange {
1981
+ name?: string | null;
1982
+ title?: string | null;
1983
+ blocks?: Array<StatPiece | ChartPiece | TablePiece | ListPiece | NotePiece | CalendarPiece | TimelinePiece | BoardPiece | HostPiece> | null;
1984
+ }
1985
+
1986
+ export interface ViewIn {
1987
+ name: string;
1988
+ title: string;
1989
+ blocks: Array<StatPiece | ChartPiece | TablePiece | ListPiece | NotePiece | CalendarPiece | TimelinePiece | BoardPiece | HostPiece>;
1990
+ }
1991
+
1992
+ export interface ViewOut {
1993
+ id: string;
1994
+ agent_id: string;
1995
+ name: string;
1996
+ title: string;
1997
+ blocks: Array<StatPiece | ChartPiece | TablePiece | ListPiece | NotePiece | CalendarPiece | TimelinePiece | BoardPiece | HostPiece>;
1998
+ created_at: string;
1999
+ updated_at: string;
2000
+ }
2001
+
2002
+ /** A view rendered for one contact: its blocks as the kit draws them, filled from the rows now, and `text`, the same in words (what a page that can't draw them shows). */
2003
+ export interface ViewRendered {
2004
+ name: string;
2005
+ title: string;
2006
+ contact_id: string;
2007
+ blocks: Array<Card | Stack | Divider | Text | Fields | Table | Listing | Stat | Chart | Note | Image | Button | Choice | Form | Calendar | Timeline | BoardBlock | AutomationBlock | FileBlock | HostBlock>;
2008
+ text: string;
2009
+ rendered_at: string;
2010
+ }
2011
+
1851
2012
  /** How an agent talks on a call (VOICE.md). The defaults are the old product's, in Spanish: its ElevenLabs voice (Carolina, from Spain), up to ten minutes, a goodbye ends it. */
1852
2013
  export interface Voice {
1853
2014
  language?: string;
@@ -1919,10 +2080,10 @@ export interface WhatsAppIn {
1919
2080
  mode?: "cloud_api" | "coexistence";
1920
2081
  }
1921
2082
 
1922
- /** One filter: the field compared with `value` by `op`. `last_days` keeps the rows whose date falls in the last `value` days; `contains` looks for words in a text. */
2083
+ /** One filter: the field compared with `value` by `op`. `last_days` keeps the rows whose date falls in the last `value` days, `next_days` those in the next `value` days (from now on); `contains` looks for words in a text. */
1923
2084
  export interface Where {
1924
2085
  field: string;
1925
- op?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "last_days";
2086
+ op?: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "last_days" | "next_days";
1926
2087
  value: string | number | boolean;
1927
2088
  }
1928
2089
 
package/src/es.ts CHANGED
@@ -233,6 +233,13 @@ export const KIT_MESSAGES_ES: KitMessages = {
233
233
  failed: "No se ha podido conectar",
234
234
  preparing: "Preparando la conexión",
235
235
  },
236
+ calendar: { allDay: "Todo el día", empty: "Nada previsto" },
237
+ board: {
238
+ move: (card) => `Mover ${card}`,
239
+ moveTo: "Mover a",
240
+ moved: (card, column) => `${card} a ${column}`,
241
+ empty: "Nada aquí",
242
+ },
236
243
  };
237
244
 
238
245
  export const BROWSER_MESSAGES_ES: BrowserMessages = {
@@ -321,6 +328,8 @@ export const CHAT_MESSAGES_ES: ChatMessages = {
321
328
  declineAll: "Rechazar todo",
322
329
  confirming: "Esperando la confirmación del negocio",
323
330
  handedOff: "Pasada a una persona",
331
+ opened: (name, title) => `${name} abrió ${title}`,
332
+ closePlaced: "Cerrar",
324
333
  handover: (holder, by, name) => {
325
334
  const automation = by.kind === "automation" ? ` por «${by.title}»` : "";
326
335
  switch (holder) {
package/src/index.ts CHANGED
@@ -144,7 +144,12 @@ export type {
144
144
  export type {
145
145
  AutomationBlock,
146
146
  Block,
147
+ BoardBlock,
148
+ BoardCard,
149
+ BoardColumn,
147
150
  ButtonBlock,
151
+ CalendarBlock,
152
+ CalendarEvent,
148
153
  CardBlock,
149
154
  ChartBlock,
150
155
  ChartPoint,
@@ -156,15 +161,19 @@ export type {
156
161
  FieldValue,
157
162
  FormBlock,
158
163
  FormField,
164
+ HostBlock,
159
165
  ImageBlock,
160
166
  ListBlock,
161
167
  NoteBlock,
162
168
  Option,
169
+ Place,
163
170
  StackBlock,
164
171
  StatBlock,
165
172
  Surface as SurfaceData,
166
173
  TableBlock,
167
174
  TextBlock,
175
+ TimelineBlock,
176
+ TimelineItem,
168
177
  Tone,
169
178
  } from "./blocks";
170
179
  export { ApiError, createClient } from "./client";
@@ -179,6 +188,8 @@ export { FILES_PER_MESSAGE, TOOL_ERROR_MAX, toolResultOf } from "./frames";
179
188
  export type { Frame, ToolResult, UpFrame } from "./frames";
180
189
  export { Button } from "./kit/Button";
181
190
  export type { ButtonProps } from "./kit/Button";
191
+ export { Agenda, daysOf, hoursOf } from "./kit/Agenda";
192
+ export { Kanban, placed } from "./kit/Kanban";
182
193
  export { Card } from "./kit/Card";
183
194
  export { Chips, Choices } from "./kit/Choices";
184
195
  export { Combobox } from "./kit/Combobox";
@@ -203,7 +214,7 @@ export { Mark } from "./kit/Mark";
203
214
  export type { MarkProps } from "./kit/Mark";
204
215
  export { MarkStack } from "./kit/MarkStack";
205
216
  export { KIT_MESSAGES } from "./kit/messages";
206
- export type { KitLocale, KitMessages } from "./kit/messages";
217
+ export type { BoardMessages, CalendarMessages, KitLocale, KitMessages } from "./kit/messages";
207
218
  export { Note } from "./kit/Note";
208
219
  export { RangeField } from "./kit/RangeField";
209
220
  export { RatingField } from "./kit/RatingField";
@@ -213,11 +224,22 @@ export { AudioPlayer } from "./kit/AudioPlayer";
213
224
  export type { AudioPlayerProps } from "./kit/AudioPlayer";
214
225
  export type { SwitchProps } from "./kit/Switch";
215
226
  export { Tag } from "./kit/Tag";
227
+ export { formatWhen, Timeline } from "./kit/Timeline";
216
228
  export { Tile, Tiles } from "./kit/Tile";
217
229
  export type { TileProps } from "./kit/Tile";
218
230
  export { DateTimeField, TimeField } from "./kit/TimeField";
219
231
  export type { DateTimeFieldProps, TimeFieldProps } from "./kit/TimeField";
220
- export type { BlockProps, SurfaceComponents, SurfacePart } from "./surface/scope";
232
+ export type {
233
+ BlockProps,
234
+ HostBlockProps,
235
+ HostBlocks,
236
+ SurfaceComponents,
237
+ SurfacePart,
238
+ } from "./surface/scope";
239
+ export { EVERY_PLACE, placeOf } from "./surface/places";
240
+ export type { SurfacePlaces } from "./surface/places";
241
+ export { Placed } from "./surface/Placed";
242
+ export type { PlacedPart } from "./surface/Placed";
221
243
  export { Surface } from "./surface/Surface";
222
244
  export type { SurfaceProps } from "./surface/Surface";
223
245
  export { chosen, filled, labelOf, picked, pressed, problemOf } from "./surface/values";
@@ -263,6 +285,7 @@ export type {
263
285
  StepState,
264
286
  StoredFile,
265
287
  ToolCall,
288
+ ViewRendered,
266
289
  Weekday,
267
290
  WeekHours,
268
291
  WidgetConfig,
@@ -271,6 +294,8 @@ export type {
271
294
  } from "./types";
272
295
  export { WIDGET_FONTS } from "./types";
273
296
  export { useAgentFace } from "./useAgentFace";
297
+ export { View, VIEW_POLL_MS, useView } from "./View";
298
+ export type { ViewProps, ViewRead } from "./View";
274
299
  export type { FaceRead } from "./useAgentFace";
275
300
  export { NO_TOOL, useConversation } from "./useConversation";
276
301
  export type { LiveConversation, ToolHandler } from "./useConversation";
@@ -0,0 +1,161 @@
1
+ import { Fragment, type ReactNode } from "react";
2
+
3
+ import type { CalendarEvent } from "../blocks";
4
+ import { useKit, useParts } from "./context";
5
+ import { dayOf, formatTime, isoDay, momentParts, TIME } from "./dates";
6
+
7
+ /** The days a calendar shows from its first: a day's, or a week's. */
8
+ export function daysOf(span: "day" | "week", start: string): string[] {
9
+ const first = dayOf(start);
10
+ if (!first) return [];
11
+ return Array.from({ length: span === "week" ? 7 : 1 }, (_, index) => {
12
+ const day = new Date(first);
13
+ day.setDate(first.getDate() + index);
14
+ return isoDay(day);
15
+ });
16
+ }
17
+
18
+ /** An event's hours as the language writes them: "10:00 – 11:00", one end, or none (a day). */
19
+ export function hoursOf(event: CalendarEvent, code: string): string | null {
20
+ const [day, from] = momentParts(event.start);
21
+ if (!TIME.test(from)) return null;
22
+ const [endDay, to] = momentParts(event.end ?? null);
23
+ const until = endDay === day && TIME.test(to) ? to : null;
24
+ return until === null
25
+ ? formatTime(from, code)
26
+ : `${formatTime(from, code)} – ${formatTime(until, code)}`;
27
+ }
28
+
29
+ /**
30
+ * A day's events, or a week's, at their hours: each day under its weekday and number (today in
31
+ * the accent), its events in order, each with its hours (or "all day"), its title and a quiet
32
+ * line of detail. In a narrow place the days go one under another, an empty one a single quiet
33
+ * line; wide enough, the week stands as seven columns. An event with a page opens it apart;
34
+ * with `onEvent`, pressing one sends it.
35
+ */
36
+ export function Agenda({
37
+ span,
38
+ start,
39
+ events,
40
+ onEvent,
41
+ disabled = false,
42
+ today = new Date(),
43
+ }: {
44
+ span: "day" | "week";
45
+ /** The first day shown, "YYYY-MM-DD". */
46
+ start: string;
47
+ events: CalendarEvent[];
48
+ onEvent?: ((event: CalendarEvent) => void) | undefined;
49
+ disabled?: boolean | undefined;
50
+ /** What counts as today; now by default. */
51
+ today?: Date | undefined;
52
+ }) {
53
+ const part = useParts();
54
+ const { code, messages } = useKit();
55
+ const now = isoDay(today);
56
+ const days = daysOf(span, start);
57
+ const weekday = new Intl.DateTimeFormat(code, { weekday: "short" });
58
+ const whole = new Intl.DateTimeFormat(code, { weekday: "long", day: "numeric", month: "long" });
59
+ return (
60
+ <div
61
+ part="agenda"
62
+ className={part("sc-agenda", ...(span === "day" ? (["sc-agenda--day"] as const) : []))}
63
+ >
64
+ <div className={part("sc-agenda__days")}>
65
+ {days.map((day) => {
66
+ const date = dayOf(day) ?? today;
67
+ const own = events.filter((event) => event.start.slice(0, 10) === day);
68
+ return (
69
+ <section
70
+ key={day}
71
+ aria-label={whole.format(date)}
72
+ className={part(
73
+ "sc-agenda__day",
74
+ ...(day === now ? (["sc-agenda__day--today"] as const) : []),
75
+ ...(own.length === 0 ? (["sc-agenda__day--empty"] as const) : []),
76
+ )}
77
+ >
78
+ {span === "week" && (
79
+ <h4 aria-hidden className={part("sc-agenda__date")}>
80
+ <span className={part("sc-agenda__weekday")}>{weekday.format(date)}</span>
81
+ <span className={part("sc-agenda__number")}>{date.getDate()}</span>
82
+ </h4>
83
+ )}
84
+ {own.length === 0 ? (
85
+ <p className={part("sc-agenda__empty")}>{messages.calendar.empty}</p>
86
+ ) : (
87
+ <ol className={part("sc-agenda__events")}>
88
+ {own.map((event) => (
89
+ <li key={event.id}>
90
+ <Event event={event} onEvent={onEvent} disabled={disabled}>
91
+ <span className={part("sc-agenda__time")}>
92
+ {(hoursOf(event, code) ?? messages.calendar.allDay)
93
+ .split(" – ")
94
+ .map((end, index) =>
95
+ // A range breaks before its dash, never inside a time ("5:00 PM").
96
+ index === 0 ? (
97
+ <span key={index}>{end}</span>
98
+ ) : (
99
+ <Fragment key={index}>
100
+ {" "}
101
+ <span>– {end}</span>
102
+ </Fragment>
103
+ ),
104
+ )}
105
+ </span>
106
+ <span className={part("sc-agenda__what")}>
107
+ <span className={part("sc-agenda__title")}>{event.title}</span>
108
+ {event.detail !== undefined && (
109
+ <span className={part("sc-agenda__detail")}>{event.detail}</span>
110
+ )}
111
+ </span>
112
+ </Event>
113
+ </li>
114
+ ))}
115
+ </ol>
116
+ )}
117
+ </section>
118
+ );
119
+ })}
120
+ </div>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ /** One event: a link to its page, a button that sends it, or just its words. */
126
+ function Event({
127
+ event,
128
+ onEvent,
129
+ disabled,
130
+ children,
131
+ }: {
132
+ event: CalendarEvent;
133
+ onEvent: ((event: CalendarEvent) => void) | undefined;
134
+ disabled: boolean;
135
+ children: ReactNode;
136
+ }) {
137
+ const part = useParts();
138
+ const className = part("sc-agenda__event");
139
+ if (event.url !== undefined && /^https:\/\//i.test(event.url)) {
140
+ return (
141
+ <a className={className} href={event.url} target="_blank" rel="noreferrer noopener">
142
+ {children}
143
+ </a>
144
+ );
145
+ }
146
+ if (onEvent !== undefined) {
147
+ return (
148
+ <button
149
+ type="button"
150
+ className={className}
151
+ disabled={disabled}
152
+ onClick={() => {
153
+ onEvent(event);
154
+ }}
155
+ >
156
+ {children}
157
+ </button>
158
+ );
159
+ }
160
+ return <div className={className}>{children}</div>;
161
+ }