@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.
- package/README.md +126 -21
- package/dist/AgentAbout.js +6 -2
- package/dist/AgentAbout.js.map +1 -1
- package/dist/AgentChat.d.ts +25 -3
- package/dist/AgentChat.js +302 -28
- package/dist/AgentChat.js.map +1 -1
- package/dist/AgentSpace.d.ts +25 -4
- package/dist/AgentSpace.js +55 -11
- package/dist/AgentSpace.js.map +1 -1
- package/dist/Threads.d.ts +73 -0
- package/dist/Threads.js +140 -0
- package/dist/Threads.js.map +1 -0
- package/dist/View.d.ts +48 -0
- package/dist/View.js +101 -0
- package/dist/View.js.map +1 -0
- package/dist/automation/marks.js +7 -3
- package/dist/automation/marks.js.map +1 -1
- package/dist/automation/messages.d.ts +4 -0
- package/dist/automation/messages.js +5 -1
- package/dist/automation/messages.js.map +1 -1
- package/dist/automation/when.js +8 -0
- package/dist/automation/when.js.map +1 -1
- package/dist/blocks.d.ts +83 -2
- package/dist/client.d.ts +23 -5
- package/dist/client.js +8 -2
- package/dist/client.js.map +1 -1
- package/dist/contract.d.ts +9 -4
- package/dist/es.d.ts +2 -0
- package/dist/es.js +34 -3
- package/dist/es.js.map +1 -1
- package/dist/frames.d.ts +7 -1
- package/dist/frames.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.js +11 -4
- package/dist/kit/Agenda.d.ts +25 -0
- package/dist/kit/Agenda.js +119 -0
- package/dist/kit/Agenda.js.map +1 -0
- package/dist/kit/Combobox.js +1 -1
- package/dist/kit/DateField.js +1 -1
- package/dist/kit/Kanban.d.ts +17 -0
- package/dist/kit/Kanban.js +157 -0
- package/dist/kit/Kanban.js.map +1 -0
- package/dist/kit/TimeField.js +1 -1
- package/dist/kit/Timeline.d.ts +14 -0
- package/dist/kit/Timeline.js +71 -0
- package/dist/kit/Timeline.js.map +1 -0
- package/dist/kit/context.d.ts +1 -1
- package/dist/kit/context.js.map +1 -1
- package/dist/kit/messages.d.ts +22 -0
- package/dist/kit/messages.js +10 -0
- package/dist/kit/messages.js.map +1 -1
- package/dist/surface/Form.js +1 -1
- package/dist/surface/Placed.d.ts +20 -0
- package/dist/surface/Placed.js +70 -0
- package/dist/surface/Placed.js.map +1 -0
- package/dist/surface/Surface.d.ts +4 -2
- package/dist/surface/Surface.js +78 -2
- package/dist/surface/Surface.js.map +1 -1
- package/dist/surface/places.d.ts +16 -0
- package/dist/surface/places.js +15 -0
- package/dist/surface/places.js.map +1 -0
- package/dist/surface/scope.d.ts +17 -1
- package/dist/surface/scope.js +1 -0
- package/dist/surface/scope.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.js.map +1 -1
- package/dist/useConversation.d.ts +2 -0
- package/dist/useConversation.js +16 -0
- package/dist/useConversation.js.map +1 -1
- package/dist/widget.js +17 -16
- package/package.json +1 -1
- package/src/AgentAbout.tsx +4 -0
- package/src/AgentChat.tsx +417 -34
- package/src/AgentSpace.tsx +106 -14
- package/src/Threads.tsx +218 -0
- package/src/View.tsx +117 -0
- package/src/automation/marks.tsx +7 -0
- package/src/automation/messages.ts +8 -0
- package/src/automation/when.ts +4 -0
- package/src/blocks.ts +100 -2
- package/src/client.ts +42 -10
- package/src/contract.ts +404 -29
- package/src/es.ts +30 -0
- package/src/frames.ts +3 -1
- package/src/index.ts +36 -3
- package/src/kit/Agenda.tsx +161 -0
- package/src/kit/Kanban.tsx +182 -0
- package/src/kit/Timeline.tsx +58 -0
- package/src/kit/context.tsx +45 -1
- package/src/kit/messages.ts +31 -0
- package/src/styles.css +825 -66
- package/src/surface/Placed.tsx +98 -0
- package/src/surface/Surface.tsx +92 -1
- package/src/surface/places.ts +19 -0
- package/src/surface/scope.ts +24 -1
- package/src/types.ts +24 -0
- package/src/useConversation.ts +10 -0
- package/src/widget/element.tsx +54 -7
- 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
|
|
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 {
|
|
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
|
+
}
|
package/src/kit/context.tsx
CHANGED
|
@@ -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
|
|