@signalcore/ui 1.0.0-beta.2 → 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 (99) hide show
  1. package/README.md +126 -21
  2. package/dist/AgentAbout.js +6 -2
  3. package/dist/AgentAbout.js.map +1 -1
  4. package/dist/AgentChat.d.ts +25 -3
  5. package/dist/AgentChat.js +302 -28
  6. package/dist/AgentChat.js.map +1 -1
  7. package/dist/AgentSpace.d.ts +25 -4
  8. package/dist/AgentSpace.js +55 -11
  9. package/dist/AgentSpace.js.map +1 -1
  10. package/dist/Threads.d.ts +73 -0
  11. package/dist/Threads.js +140 -0
  12. package/dist/Threads.js.map +1 -0
  13. package/dist/View.d.ts +48 -0
  14. package/dist/View.js +101 -0
  15. package/dist/View.js.map +1 -0
  16. package/dist/automation/marks.js +7 -3
  17. package/dist/automation/marks.js.map +1 -1
  18. package/dist/automation/messages.d.ts +4 -0
  19. package/dist/automation/messages.js +5 -1
  20. package/dist/automation/messages.js.map +1 -1
  21. package/dist/automation/when.js +8 -0
  22. package/dist/automation/when.js.map +1 -1
  23. package/dist/blocks.d.ts +83 -2
  24. package/dist/client.d.ts +23 -5
  25. package/dist/client.js +8 -2
  26. package/dist/client.js.map +1 -1
  27. package/dist/contract.d.ts +9 -4
  28. package/dist/es.d.ts +2 -0
  29. package/dist/es.js +34 -3
  30. package/dist/es.js.map +1 -1
  31. package/dist/frames.d.ts +7 -1
  32. package/dist/frames.js.map +1 -1
  33. package/dist/index.d.ts +14 -7
  34. package/dist/index.js +11 -4
  35. package/dist/kit/Agenda.d.ts +25 -0
  36. package/dist/kit/Agenda.js +119 -0
  37. package/dist/kit/Agenda.js.map +1 -0
  38. package/dist/kit/Combobox.js +1 -1
  39. package/dist/kit/DateField.js +1 -1
  40. package/dist/kit/Kanban.d.ts +17 -0
  41. package/dist/kit/Kanban.js +157 -0
  42. package/dist/kit/Kanban.js.map +1 -0
  43. package/dist/kit/TimeField.js +1 -1
  44. package/dist/kit/Timeline.d.ts +14 -0
  45. package/dist/kit/Timeline.js +71 -0
  46. package/dist/kit/Timeline.js.map +1 -0
  47. package/dist/kit/context.d.ts +1 -1
  48. package/dist/kit/context.js.map +1 -1
  49. package/dist/kit/messages.d.ts +22 -0
  50. package/dist/kit/messages.js +10 -0
  51. package/dist/kit/messages.js.map +1 -1
  52. package/dist/surface/Form.js +1 -1
  53. package/dist/surface/Placed.d.ts +20 -0
  54. package/dist/surface/Placed.js +70 -0
  55. package/dist/surface/Placed.js.map +1 -0
  56. package/dist/surface/Surface.d.ts +4 -2
  57. package/dist/surface/Surface.js +78 -2
  58. package/dist/surface/Surface.js.map +1 -1
  59. package/dist/surface/places.d.ts +16 -0
  60. package/dist/surface/places.js +15 -0
  61. package/dist/surface/places.js.map +1 -0
  62. package/dist/surface/scope.d.ts +17 -1
  63. package/dist/surface/scope.js +1 -0
  64. package/dist/surface/scope.js.map +1 -1
  65. package/dist/types.d.ts +23 -0
  66. package/dist/types.js.map +1 -1
  67. package/dist/useConversation.d.ts +2 -0
  68. package/dist/useConversation.js +16 -0
  69. package/dist/useConversation.js.map +1 -1
  70. package/dist/widget.js +17 -16
  71. package/package.json +1 -1
  72. package/src/AgentAbout.tsx +4 -0
  73. package/src/AgentChat.tsx +417 -34
  74. package/src/AgentSpace.tsx +106 -14
  75. package/src/Threads.tsx +218 -0
  76. package/src/View.tsx +117 -0
  77. package/src/automation/marks.tsx +7 -0
  78. package/src/automation/messages.ts +8 -0
  79. package/src/automation/when.ts +4 -0
  80. package/src/blocks.ts +100 -2
  81. package/src/client.ts +42 -10
  82. package/src/contract.ts +404 -29
  83. package/src/es.ts +30 -0
  84. package/src/frames.ts +3 -1
  85. package/src/index.ts +36 -3
  86. package/src/kit/Agenda.tsx +161 -0
  87. package/src/kit/Kanban.tsx +182 -0
  88. package/src/kit/Timeline.tsx +58 -0
  89. package/src/kit/context.tsx +45 -1
  90. package/src/kit/messages.ts +31 -0
  91. package/src/styles.css +825 -66
  92. package/src/surface/Placed.tsx +98 -0
  93. package/src/surface/Surface.tsx +92 -1
  94. package/src/surface/places.ts +19 -0
  95. package/src/surface/scope.ts +24 -1
  96. package/src/types.ts +24 -0
  97. package/src/useConversation.ts +10 -0
  98. package/src/widget/element.tsx +54 -7
  99. package/src/widget/widget.css +13 -0
package/src/es.ts CHANGED
@@ -10,6 +10,7 @@ import type { TaskMessages } from "./AgentTasks";
10
10
  import type { AutomationMessages, DayRef } from "./automation/messages";
11
11
  import type { BrowserMessages } from "./BrowserView";
12
12
  import type { KitMessages } from "./kit/messages";
13
+ import type { ThreadMessages } from "./Threads";
13
14
  import type { VoiceCallMessages } from "./VoiceCall";
14
15
 
15
16
  /** The article before a time: "la 1:00", "las 8:00". */
@@ -76,6 +77,8 @@ export const AUTOMATION_MESSAGES_ES: AutomationMessages = {
76
77
  on: (often, when) => `${often}, ${when.charAt(0).toLowerCase()}${when.slice(1)}`,
77
78
  custom: "Con un horario propio",
78
79
  webhook: "Cuando otro sistema la llama",
80
+ manual: "Cuando alguien la inicia",
81
+ after: (automation) => (automation ? `Tras «${automation}»` : "Tras otra automatización"),
79
82
  platform: "Cuando llega un mensaje",
80
83
  event: (app, event) =>
81
84
  event
@@ -118,6 +121,8 @@ export const AUTOMATION_MESSAGES_ES: AutomationMessages = {
118
121
  platform: "un mensaje",
119
122
  agent: "se lo pidieron en el chat",
120
123
  test: "prueba",
124
+ api: "tu app",
125
+ after: "tras otra",
121
126
  })[by],
122
127
  statuses: {
123
128
  running: "En curso",
@@ -228,6 +233,13 @@ export const KIT_MESSAGES_ES: KitMessages = {
228
233
  failed: "No se ha podido conectar",
229
234
  preparing: "Preparando la conexión",
230
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
+ },
231
243
  };
232
244
 
233
245
  export const BROWSER_MESSAGES_ES: BrowserMessages = {
@@ -310,8 +322,14 @@ export const CHAT_MESSAGES_ES: ChatMessages = {
310
322
  inputs: {},
311
323
  allow: "Aprobar",
312
324
  decline: "Rechazar",
325
+ approvals: (count) => `${count} acciones esperan tu aprobación`,
326
+ approveAll: "Aprobar todo",
327
+ approveSome: (count) => `Aprobar ${count}`,
328
+ declineAll: "Rechazar todo",
313
329
  confirming: "Esperando la confirmación del negocio",
314
330
  handedOff: "Pasada a una persona",
331
+ opened: (name, title) => `${name} abrió ${title}`,
332
+ closePlaced: "Cerrar",
315
333
  handover: (holder, by, name) => {
316
334
  const automation = by.kind === "automation" ? ` por «${by.title}»` : "";
317
335
  switch (holder) {
@@ -417,6 +435,17 @@ export const TASK_MESSAGES_ES: TaskMessages = {
417
435
  failed: "No se ha podido cambiar. Inténtalo de nuevo.",
418
436
  };
419
437
 
438
+ export const THREAD_MESSAGES_ES: ThreadMessages = {
439
+ open: "Conversaciones",
440
+ title: "Tus conversaciones",
441
+ back: "Volver a la conversación",
442
+ new: "Nueva conversación",
443
+ untitled: "Conversación nueva",
444
+ unread: (count) => (count === 1 ? "1 mensaje sin leer" : `${count} mensajes sin leer`),
445
+ waiting: "Te espera",
446
+ empty: "Aún no hay nada aquí.",
447
+ };
448
+
420
449
  export const SPACE_MESSAGES_ES: SpaceMessages = {
421
450
  modes: { label: "Modo", text: "Texto", voice: "Voz" },
422
451
  again: "Volver a llamar",
@@ -428,4 +457,5 @@ export const SPACE_MESSAGES_ES: SpaceMessages = {
428
457
  chat: CHAT_MESSAGES_ES,
429
458
  voice: VOICE_CALL_MESSAGES_ES,
430
459
  tasks: TASK_MESSAGES_ES,
460
+ threads: THREAD_MESSAGES_ES,
431
461
  };
package/src/frames.ts CHANGED
@@ -52,13 +52,15 @@ export type Frame =
52
52
 
53
53
  /**
54
54
  * Up: the person says something (words, files they uploaded to the conversation, or both), acts
55
- * on a surface, or stops the reply on its way; takes the browser or gives it back, and acts on it
55
+ * on a surface (several things at once: a yes or no on each call a reply waits on), or stops
56
+ * the reply on its way; takes the browser or gives it back, and acts on it
56
57
  * while they hold it. And what a tool of the host's answered, run in the page: its output, or
57
58
  * why it failed.
58
59
  */
59
60
  export type UpFrame =
60
61
  | { type: "say"; text: string; files?: string[] }
61
62
  | ({ type: "action" } & Action)
63
+ | { type: "actions"; actions: ({ type: "action" } & Action)[] }
62
64
  | { type: "stop" }
63
65
  | { type: "browser.control"; to: BrowserControl }
64
66
  | { type: "browser.close" }
package/src/index.ts CHANGED
@@ -97,6 +97,14 @@ export type {
97
97
  export { taskCard } from "./automation/types";
98
98
  export { AgentTasks, TASK_MESSAGES, TASKS_POLL_MS, useTasks } from "./AgentTasks";
99
99
  export type { AgentTasksProps, TaskMessages, TaskPart } from "./AgentTasks";
100
+ export { Conversation, THREAD_MESSAGES, THREADS_POLL_MS, Threads, useUnread } from "./Threads";
101
+ export type {
102
+ ConversationProps,
103
+ ThreadMessages,
104
+ ThreadPart,
105
+ ThreadsProps,
106
+ Unread,
107
+ } from "./Threads";
100
108
  export { AgentCard } from "./AgentCard";
101
109
  export type { AgentCardProps, CardField, CardPart } from "./AgentCard";
102
110
  export { AgentChat, CHAT_MESSAGES } from "./AgentChat";
@@ -110,6 +118,7 @@ export {
110
118
  KIT_MESSAGES_ES,
111
119
  SPACE_MESSAGES_ES,
112
120
  TASK_MESSAGES_ES,
121
+ THREAD_MESSAGES_ES,
113
122
  VOICE_CALL_MESSAGES_ES,
114
123
  } from "./es";
115
124
  export { VOICE_CALL_MESSAGES, VoiceCall } from "./VoiceCall";
@@ -135,7 +144,12 @@ export type {
135
144
  export type {
136
145
  AutomationBlock,
137
146
  Block,
147
+ BoardBlock,
148
+ BoardCard,
149
+ BoardColumn,
138
150
  ButtonBlock,
151
+ CalendarBlock,
152
+ CalendarEvent,
139
153
  CardBlock,
140
154
  ChartBlock,
141
155
  ChartPoint,
@@ -147,15 +161,19 @@ export type {
147
161
  FieldValue,
148
162
  FormBlock,
149
163
  FormField,
164
+ HostBlock,
150
165
  ImageBlock,
151
166
  ListBlock,
152
167
  NoteBlock,
153
168
  Option,
169
+ Place,
154
170
  StackBlock,
155
171
  StatBlock,
156
172
  Surface as SurfaceData,
157
173
  TableBlock,
158
174
  TextBlock,
175
+ TimelineBlock,
176
+ TimelineItem,
159
177
  Tone,
160
178
  } from "./blocks";
161
179
  export { ApiError, createClient } from "./client";
@@ -170,6 +188,8 @@ export { FILES_PER_MESSAGE, TOOL_ERROR_MAX, toolResultOf } from "./frames";
170
188
  export type { Frame, ToolResult, UpFrame } from "./frames";
171
189
  export { Button } from "./kit/Button";
172
190
  export type { ButtonProps } from "./kit/Button";
191
+ export { Agenda, daysOf, hoursOf } from "./kit/Agenda";
192
+ export { Kanban, placed } from "./kit/Kanban";
173
193
  export { Card } from "./kit/Card";
174
194
  export { Chips, Choices } from "./kit/Choices";
175
195
  export { Combobox } from "./kit/Combobox";
@@ -194,7 +214,7 @@ export { Mark } from "./kit/Mark";
194
214
  export type { MarkProps } from "./kit/Mark";
195
215
  export { MarkStack } from "./kit/MarkStack";
196
216
  export { KIT_MESSAGES } from "./kit/messages";
197
- export type { KitLocale, KitMessages } from "./kit/messages";
217
+ export type { BoardMessages, CalendarMessages, KitLocale, KitMessages } from "./kit/messages";
198
218
  export { Note } from "./kit/Note";
199
219
  export { RangeField } from "./kit/RangeField";
200
220
  export { RatingField } from "./kit/RatingField";
@@ -204,11 +224,22 @@ export { AudioPlayer } from "./kit/AudioPlayer";
204
224
  export type { AudioPlayerProps } from "./kit/AudioPlayer";
205
225
  export type { SwitchProps } from "./kit/Switch";
206
226
  export { Tag } from "./kit/Tag";
227
+ export { formatWhen, Timeline } from "./kit/Timeline";
207
228
  export { Tile, Tiles } from "./kit/Tile";
208
229
  export type { TileProps } from "./kit/Tile";
209
230
  export { DateTimeField, TimeField } from "./kit/TimeField";
210
231
  export type { DateTimeFieldProps, TimeFieldProps } from "./kit/TimeField";
211
- 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";
212
243
  export { Surface } from "./surface/Surface";
213
244
  export type { SurfaceProps } from "./surface/Surface";
214
245
  export { chosen, filled, labelOf, picked, pressed, problemOf } from "./surface/values";
@@ -235,7 +266,6 @@ export type {
235
266
  ChannelWeb,
236
267
  ContactGlance,
237
268
  ConversationPerson,
238
- Conversation,
239
269
  ConversationDetail,
240
270
  ConversationFile,
241
271
  Decides,
@@ -255,6 +285,7 @@ export type {
255
285
  StepState,
256
286
  StoredFile,
257
287
  ToolCall,
288
+ ViewRendered,
258
289
  Weekday,
259
290
  WeekHours,
260
291
  WidgetConfig,
@@ -263,6 +294,8 @@ export type {
263
294
  } from "./types";
264
295
  export { WIDGET_FONTS } from "./types";
265
296
  export { useAgentFace } from "./useAgentFace";
297
+ export { View, VIEW_POLL_MS, useView } from "./View";
298
+ export type { ViewProps, ViewRead } from "./View";
266
299
  export type { FaceRead } from "./useAgentFace";
267
300
  export { NO_TOOL, useConversation } from "./useConversation";
268
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
+ }
@@ -0,0 +1,182 @@
1
+ import { ArrowsLeftRightIcon } from "@phosphor-icons/react";
2
+ import { useState, type DragEvent } from "react";
3
+
4
+ import type { BoardCard, BoardColumn } from "../blocks";
5
+ import { useKit, useParts } from "./context";
6
+ import { Popover, usePopover } from "./Popover";
7
+
8
+ /** What a dragged card carries, so a drop elsewhere on the page takes nothing of it. */
9
+ const CARD = "application/x-signalcore-card";
10
+
11
+ /** The columns with each card where the person last put it (`moved`, card id to column id). */
12
+ export function placed(
13
+ columns: readonly BoardColumn[],
14
+ moved: Readonly<Record<string, string>>,
15
+ ): BoardColumn[] {
16
+ const cards = columns.flatMap((column) =>
17
+ column.cards.map((card) => ({ card, column: moved[card.id] ?? column.id })),
18
+ );
19
+ return columns.map((column) => ({
20
+ ...column,
21
+ cards: cards.filter((each) => each.column === column.id).map((each) => each.card),
22
+ }));
23
+ }
24
+
25
+ /**
26
+ * Cards in columns, a column per stage, side by side, scrolling across when they don't fit: each
27
+ * column's title and how many it holds over its cards (a title, a quieter line, a detail at its
28
+ * foot). With `onMove`, the person moves a card to another column by dragging it there, or from
29
+ * its button's list of the other columns (the keyboard's way); the card goes there at once.
30
+ */
31
+ export function Kanban({
32
+ columns,
33
+ onMove,
34
+ disabled = false,
35
+ }: {
36
+ columns: BoardColumn[];
37
+ onMove?: ((card: BoardCard, column: BoardColumn) => void) | undefined;
38
+ disabled?: boolean | undefined;
39
+ }) {
40
+ const part = useParts();
41
+ const { messages } = useKit();
42
+ const [moved, setMoved] = useState<Record<string, string>>({});
43
+ const [dragging, setDragging] = useState<string | null>(null);
44
+ const [over, setOver] = useState<string | null>(null);
45
+ const movable = onMove !== undefined && !disabled;
46
+ const shown = placed(columns, moved);
47
+ const move = (card: BoardCard, to: BoardColumn) => {
48
+ const from = shown.find((column) => column.cards.some((each) => each.id === card.id));
49
+ if (!onMove || from?.id === to.id) return;
50
+ setMoved((before) => ({ ...before, [card.id]: to.id }));
51
+ onMove(card, to);
52
+ };
53
+ const dropOn = (column: BoardColumn) => (event: DragEvent) => {
54
+ event.preventDefault();
55
+ setOver(null);
56
+ setDragging(null);
57
+ const id = event.dataTransfer.getData(CARD);
58
+ const card = columns.flatMap((each) => each.cards).find((each) => each.id === id);
59
+ if (card) move(card, column);
60
+ };
61
+ return (
62
+ <div part="board" className={part("sc-board")}>
63
+ {shown.map((column) => (
64
+ <section
65
+ key={column.id}
66
+ aria-label={column.title}
67
+ className={part(
68
+ "sc-board__column",
69
+ ...(over === column.id ? (["sc-board__column--over"] as const) : []),
70
+ )}
71
+ onDragOver={(event) => {
72
+ if (!movable || dragging === null) return;
73
+ event.preventDefault();
74
+ event.dataTransfer.dropEffect = "move";
75
+ setOver(column.id);
76
+ }}
77
+ onDragLeave={(event) => {
78
+ const to = event.relatedTarget;
79
+ if (!(to instanceof Node && event.currentTarget.contains(to))) setOver(null);
80
+ }}
81
+ onDrop={movable ? dropOn(column) : undefined}
82
+ >
83
+ <h4 className={part("sc-board__head")}>
84
+ <span className={part("sc-board__title")}>{column.title}</span>
85
+ <span className={part("sc-board__count")}>{column.cards.length}</span>
86
+ </h4>
87
+ {column.cards.length === 0 ? (
88
+ <p className={part("sc-board__empty")}>{messages.board.empty}</p>
89
+ ) : (
90
+ <ul className={part("sc-board__cards")}>
91
+ {column.cards.map((card) => (
92
+ <li
93
+ key={card.id}
94
+ draggable={movable}
95
+ onDragStart={(event) => {
96
+ event.dataTransfer.setData(CARD, card.id);
97
+ event.dataTransfer.effectAllowed = "move";
98
+ setDragging(card.id);
99
+ }}
100
+ onDragEnd={() => {
101
+ setDragging(null);
102
+ setOver(null);
103
+ }}
104
+ className={part(
105
+ "sc-board__card",
106
+ ...(dragging === card.id ? (["sc-board__card--dragging"] as const) : []),
107
+ )}
108
+ >
109
+ <span className={part("sc-board__card-title")}>{card.title}</span>
110
+ {card.subtitle !== undefined && (
111
+ <span className={part("sc-board__card-subtitle")}>{card.subtitle}</span>
112
+ )}
113
+ {card.meta !== undefined && (
114
+ <span className={part("sc-board__card-meta")}>{card.meta}</span>
115
+ )}
116
+ {movable && (
117
+ <Move
118
+ card={card}
119
+ to={shown.filter((other) => other.id !== column.id)}
120
+ onMove={move}
121
+ />
122
+ )}
123
+ </li>
124
+ ))}
125
+ </ul>
126
+ )}
127
+ </section>
128
+ ))}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ /** A card's button that opens the other columns, to move it to one of them. */
134
+ function Move({
135
+ card,
136
+ to,
137
+ onMove,
138
+ }: {
139
+ card: BoardCard;
140
+ to: BoardColumn[];
141
+ onMove: (card: BoardCard, column: BoardColumn) => void;
142
+ }) {
143
+ const part = useParts();
144
+ const { messages } = useKit();
145
+ const [open, setOpen] = useState(false);
146
+ const popover = usePopover(open, setOpen, { width: "own", role: "dialog" });
147
+ return (
148
+ <>
149
+ <button
150
+ ref={popover.refs.setReference}
151
+ type="button"
152
+ aria-label={messages.board.move(card.title)}
153
+ title={messages.board.move(card.title)}
154
+ aria-expanded={open}
155
+ className={part("sc-board__move")}
156
+ {...popover.getReferenceProps({ onClick: () => setOpen(!open) })}
157
+ >
158
+ <ArrowsLeftRightIcon aria-hidden />
159
+ </button>
160
+ {open && (
161
+ <Popover popover={popover} focus label={messages.board.moveTo}>
162
+ <div className={part("sc-board__menu")}>
163
+ <p>{messages.board.moveTo}</p>
164
+ {to.map((column) => (
165
+ <button
166
+ key={column.id}
167
+ type="button"
168
+ className={part("sc-listbox__item")}
169
+ onClick={() => {
170
+ setOpen(false);
171
+ onMove(card, column);
172
+ }}
173
+ >
174
+ {column.title}
175
+ </button>
176
+ ))}
177
+ </div>
178
+ </Popover>
179
+ )}
180
+ </>
181
+ );
182
+ }
@@ -0,0 +1,58 @@
1
+ import type { TimelineItem } from "../blocks";
2
+ import { useKit, useParts } from "./context";
3
+ import { dayOf, momentParts, TIME } from "./dates";
4
+
5
+ /** When a timeline's item was, short: "20 oct, 16:00"; its year when not this one's. */
6
+ export function formatWhen(at: string, code: string, today: Date = new Date()): string {
7
+ const [date, time] = momentParts(at);
8
+ const day = dayOf(date);
9
+ if (!day) return at;
10
+ const timed = TIME.test(time);
11
+ if (timed) {
12
+ const [hours = 0, minutes = 0] = time.split(":").map(Number);
13
+ day.setHours(hours, minutes);
14
+ }
15
+ return new Intl.DateTimeFormat(code, {
16
+ day: "numeric",
17
+ month: "short",
18
+ ...(day.getFullYear() !== today.getFullYear() && { year: "numeric" }),
19
+ ...(timed && { hour: "numeric", minute: "2-digit" }),
20
+ }).format(day);
21
+ }
22
+
23
+ /**
24
+ * Dated things, one under another along a hairline, the newest first: each a dot on the line (in
25
+ * its tone, when it has one), its title with where it stands in a quiet word at its right, when
26
+ * it was under it, and a line about it.
27
+ */
28
+ export function Timeline({ items }: { items: TimelineItem[] }) {
29
+ const part = useParts();
30
+ const { code } = useKit();
31
+ return (
32
+ <ol part="timeline" className={part("sc-timeline")}>
33
+ {items.map((item, index) => (
34
+ <li
35
+ key={item.id ?? index}
36
+ className={part(
37
+ "sc-timeline__item",
38
+ ...(item.tone ? ([`sc-timeline__item--${item.tone}`] as const) : []),
39
+ )}
40
+ >
41
+ <span aria-hidden className={part("sc-timeline__dot")} />
42
+ <div className={part("sc-timeline__body")}>
43
+ <p className={part("sc-timeline__head")}>
44
+ <span className={part("sc-timeline__title")}>{item.title}</span>
45
+ {item.status !== undefined && (
46
+ <span className={part("sc-timeline__status")}>{item.status}</span>
47
+ )}
48
+ </p>
49
+ <time dateTime={item.at} className={part("sc-timeline__when")}>
50
+ {formatWhen(item.at, code)}
51
+ </time>
52
+ {item.text !== undefined && <p className={part("sc-timeline__text")}>{item.text}</p>}
53
+ </div>
54
+ </li>
55
+ ))}
56
+ </ol>
57
+ );
58
+ }
@@ -186,7 +186,51 @@ export type KitPart =
186
186
  | "sc-run__error"
187
187
  | "sc-run__live"
188
188
  | "sc-run__links"
189
- | "sc-run__empty";
189
+ | "sc-run__empty"
190
+ | "sc-agenda"
191
+ | "sc-agenda--day"
192
+ | "sc-agenda__days"
193
+ | "sc-agenda__day"
194
+ | "sc-agenda__day--today"
195
+ | "sc-agenda__day--empty"
196
+ | "sc-agenda__date"
197
+ | "sc-agenda__weekday"
198
+ | "sc-agenda__number"
199
+ | "sc-agenda__events"
200
+ | "sc-agenda__event"
201
+ | "sc-agenda__time"
202
+ | "sc-agenda__what"
203
+ | "sc-agenda__title"
204
+ | "sc-agenda__detail"
205
+ | "sc-agenda__empty"
206
+ | "sc-timeline"
207
+ | "sc-timeline__item"
208
+ | "sc-timeline__item--info"
209
+ | "sc-timeline__item--success"
210
+ | "sc-timeline__item--warning"
211
+ | "sc-timeline__item--danger"
212
+ | "sc-timeline__dot"
213
+ | "sc-timeline__body"
214
+ | "sc-timeline__head"
215
+ | "sc-timeline__title"
216
+ | "sc-timeline__status"
217
+ | "sc-timeline__when"
218
+ | "sc-timeline__text"
219
+ | "sc-board"
220
+ | "sc-board__column"
221
+ | "sc-board__column--over"
222
+ | "sc-board__head"
223
+ | "sc-board__title"
224
+ | "sc-board__count"
225
+ | "sc-board__cards"
226
+ | "sc-board__card"
227
+ | "sc-board__card--dragging"
228
+ | "sc-board__card-title"
229
+ | "sc-board__card-subtitle"
230
+ | "sc-board__card-meta"
231
+ | "sc-board__move"
232
+ | "sc-board__menu"
233
+ | "sc-board__empty";
190
234
 
191
235
  export type KitClassNames = Partial<Record<KitPart, string>> | undefined;
192
236